Skip to content
Open
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
30 changes: 27 additions & 3 deletions packages/time-series/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Here, we'll create a new time series "`temperature`":
```javascript

import { createClient } from 'redis';
import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding, TimeSeriesAggregationType } from '@redis/time-series';
import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding, TIME_SERIES_AGGREGATION_TYPE } from '@redis/time-series';

...
const created = await client.ts.create('temperature', {
Expand Down Expand Up @@ -82,7 +82,7 @@ const toTimestamp = 1640995260000; // Jan 1 2022 00:01:00
const rangeResponse = await client.ts.range('temperature', fromTimestamp, toTimestamp, {
// Group into 10 second averages.
AGGREGATION: {
type: TimeSeriesAggregationType.AVERAGE,
type: TIME_SERIES_AGGREGATION_TYPE.AVG,
timeBucket: 10000
}
});
Expand All @@ -100,6 +100,31 @@ console.log('RANGE RESPONSE:');
// ]
```

For multiple aggregations in one command, use the dedicated `*MultiAggr` methods:

```javascript
const multiRangeResponse = await client.ts.rangeMultiAggr('temperature', fromTimestamp, toTimestamp, {
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MIN,
TIME_SERIES_AGGREGATION_TYPE.MAX,
TIME_SERIES_AGGREGATION_TYPE.AVG
],
timeBucket: 10000
}
});

// multiRangeResponse looks like:
// [
// { timestamp: 1640995200000, values: [120, 580, 356.8] },
// ...
// ]
```

Equivalent multi-aggregation helpers are also available for reverse and multi-key variants:
`revRangeMultiAggr`, `mRangeMultiAggr`, `mRevRangeMultiAggr`, `mRangeWithLabelsMultiAggr`,
`mRevRangeWithLabelsMultiAggr`, `mRangeSelectedLabelsMultiAggr`, and `mRevRangeSelectedLabelsMultiAggr`.

### Altering Time Series data Stored in Redis

RedisTimeSeries includes commands that can update values in a time series data structure.
Expand Down Expand Up @@ -140,4 +165,3 @@ const tsInfo = await client.ts.info('temperature');
// rules: []
// }
```

76 changes: 76 additions & 0 deletions packages/time-series/lib/commands/MRANGE_MULTIAGGR.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import MRANGE_MULTIAGGR from './MRANGE_MULTIAGGR';
import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
import { parseArgs } from '@redis/client/lib/commands/generic-transformers';

describe('TS.MRANGE_MULTIAGGR', () => {
it('transformArguments', () => {
assert.deepEqual(
parseArgs(MRANGE_MULTIAGGR, '-', '+', 'label=value', {
LATEST: true,
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 0,
max: 1
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MIN,
TIME_SERIES_AGGREGATION_TYPE.MAX
],
timeBucket: 1
}
}),
[
'TS.MRANGE', '-', '+',
'LATEST',
'FILTER_BY_TS', '0',
'FILTER_BY_VALUE', '0', '1',
'COUNT', '1',
'ALIGN', '-',
'AGGREGATION', 'MIN,MAX', '1',
'FILTER', 'label=value'
]
);
});

testUtils.testWithClient('client.ts.mRangeMultiAggr', async client => {
await client.ts.add('mrange-multi', 1000, 0, {
LABELS: {
label: 'value'
}
});
await client.ts.add('mrange-multi', 1001, 1);
await client.ts.add('mrange-multi', 1002, 2);

const reply = await client.ts.mRangeMultiAggr('-', '+', 'label=value', {
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MIN,
TIME_SERIES_AGGREGATION_TYPE.MAX
],
timeBucket: 10
}
});

assert.deepStrictEqual(
reply,
Object.create(null, {
'mrange-multi': {
configurable: true,
enumerable: true,
value: [{
timestamp: 1000,
values: [0, 2]
}]
}
})
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [8, 8]
});
});
79 changes: 79 additions & 0 deletions packages/time-series/lib/commands/MRANGE_MULTIAGGR.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 { 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,
MultiAggregationSampleRawReply,
Timestamp,
transformMultiAggregationSamplesReply
} from './helpers';
import { TsRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR';
import { parseFilterArgument } from './MGET';

export type TsMRangeMultiRawReply2 = ArrayReply<
TuplesReply<[
key: BlobStringReply,
labels: never, // empty array without WITHLABELS or SELECTED_LABELS
samples: ArrayReply<Resp2Reply<MultiAggregationSampleRawReply>>
]>
>;

export type TsMRangeMultiRawReply3 = MapReply<
BlobStringReply,
TuplesReply<[
labels: never, // empty hash without WITHLABELS or SELECTED_LABELS
metadata: never, // ?!
samples: ArrayReply<MultiAggregationSampleRawReply>
]>
>;

/**
* Creates a function that parses arguments for multi-range commands with multiple aggregators
* @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
*/
export function createTransformMRangeMultiArguments(command: RedisArgument) {
return (
parser: CommandParser,
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
filter: RedisVariadicArgument,
options: TsRangeMultiAggrOptions
) => {
parser.push(command);
parseRangeMultiArguments(
parser,
fromTimestamp,
toTimestamp,
options
);

parseFilterArgument(parser, filter);
};
}

export default {
NOT_KEYED_COMMAND: true,
IS_READ_ONLY: true,
/**
* Gets multi-aggregation samples for time series matching a specific filter within a time range
* @param parser - The command parser
* @param fromTimestamp - Start timestamp for range
* @param toTimestamp - End timestamp for range
* @param filter - Filter to match time series keys
* @param options - Optional parameters for the command
*/
parseCommand: createTransformMRangeMultiArguments('TS.MRANGE'),
transformReply: {
2(reply: TsMRangeMultiRawReply2, _?: any, typeMapping?: TypeMapping) {
return resp2MapToValue(reply, ([_key, _labels, samples]) => {
return transformMultiAggregationSamplesReply[2](samples);
}, typeMapping);
},
3(reply: TsMRangeMultiRawReply3) {
return resp3MapToValue(reply, ([_labels, _metadata, samples]) => {
return transformMultiAggregationSamplesReply[3](samples);
});
}
},
} as const satisfies Command;
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import MRANGE_SELECTED_LABELS_MULTIAGGR from './MRANGE_SELECTED_LABELS_MULTIAGGR';
import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE';
import { parseArgs } from '@redis/client/lib/commands/generic-transformers';

describe('TS.MRANGE_SELECTED_LABELS_MULTIAGGR', () => {
it('transformArguments', () => {
assert.deepEqual(
parseArgs(MRANGE_SELECTED_LABELS_MULTIAGGR, '-', '+', 'label', 'label=value', {
FILTER_BY_TS: [0],
FILTER_BY_VALUE: {
min: 0,
max: 1
},
COUNT: 1,
ALIGN: '-',
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MIN,
TIME_SERIES_AGGREGATION_TYPE.MAX
],
timeBucket: 1
}
}),
[
'TS.MRANGE', '-', '+',
'FILTER_BY_TS', '0',
'FILTER_BY_VALUE', '0', '1',
'COUNT', '1',
'ALIGN', '-',
'AGGREGATION', 'MIN,MAX', '1',
'SELECTED_LABELS', 'label',
'FILTER', 'label=value'
]
);
});

testUtils.testWithClient('client.ts.mRangeSelectedLabelsMultiAggr', async client => {
await client.ts.add('mrange-selectedlabels-multi', 1000, 0, {
LABELS: { label: 'value' }
});
await client.ts.add('mrange-selectedlabels-multi', 1001, 1);
await client.ts.add('mrange-selectedlabels-multi', 1002, 2);

const reply = await client.ts.mRangeSelectedLabelsMultiAggr('-', '+', ['label', 'NX'], 'label=value', {
AGGREGATION: {
types: [
TIME_SERIES_AGGREGATION_TYPE.MIN,
TIME_SERIES_AGGREGATION_TYPE.MAX
],
timeBucket: 10
}
});

assert.deepStrictEqual(
reply,
Object.create(null, {
'mrange-selectedlabels-multi': {
configurable: true,
enumerable: true,
value: {
labels: Object.create(null, {
label: {
configurable: true,
enumerable: true,
value: 'value'
},
NX: {
configurable: true,
enumerable: true,
value: null
}
}),
samples: [{
timestamp: 1000,
values: [0, 2]
}]
}
}
})
);
}, {
...GLOBAL.SERVERS.OPEN,
minimumDockerVersion: [8, 8]
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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,
MultiAggregationSampleRawReply,
Timestamp,
transformRESP2Labels,
transformMultiAggregationSamplesReply
} from './helpers';
import { TsRangeMultiAggrOptions, parseRangeMultiArguments } from './RANGE_MULTIAGGR';
import { parseFilterArgument } from './MGET';

export type TsMRangeSelectedLabelsMultiRawReply2 = ArrayReply<
TuplesReply<[
key: BlobStringReply,
labels: ArrayReply<TuplesReply<[
label: BlobStringReply,
value: BlobStringReply | NullReply
]>>,
samples: ArrayReply<Resp2Reply<MultiAggregationSampleRawReply>>
]>
>;

export type TsMRangeSelectedLabelsMultiRawReply3 = MapReply<
BlobStringReply,
TuplesReply<[
labels: MapReply<BlobStringReply, BlobStringReply | NullReply>,
metadata: never, // ?!
samples: ArrayReply<MultiAggregationSampleRawReply>
]>
>;

/**
* Creates a function that parses arguments for multi-range commands with selected labels and multiple aggregators
* @param command - The command name to use (TS.MRANGE or TS.MREVRANGE)
*/
export function createTransformMRangeSelectedLabelsMultiArguments(command: RedisArgument) {
return (
parser: CommandParser,
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
selectedLabels: RedisVariadicArgument,
filter: RedisVariadicArgument,
options: TsRangeMultiAggrOptions
) => {
parser.push(command);
parseRangeMultiArguments(
parser,
fromTimestamp,
toTimestamp,
options
);

parseSelectedLabelsArguments(parser, selectedLabels);

parseFilterArgument(parser, filter);
};
}

export default {
NOT_KEYED_COMMAND: true,
IS_READ_ONLY: true,
Comment thread
cursor[bot] marked this conversation as resolved.
/**
* Gets multi-aggregation samples for time series matching a filter with selected labels
* @param parser - The command parser
* @param fromTimestamp - Start timestamp for range
* @param toTimestamp - End timestamp for range
* @param selectedLabels - Labels to include in the output
* @param filter - Filter to match time series keys
* @param options - Optional parameters for the command
*/
parseCommand: createTransformMRangeSelectedLabelsMultiArguments('TS.MRANGE'),
transformReply: {
2(reply: TsMRangeSelectedLabelsMultiRawReply2, _?: any, typeMapping?: TypeMapping) {
return resp2MapToValue(reply, ([_key, labels, samples]) => {
return {
labels: transformRESP2Labels(labels, typeMapping),
samples: transformMultiAggregationSamplesReply[2](samples)
};
}, typeMapping);
},
3(reply: TsMRangeSelectedLabelsMultiRawReply3) {
return resp3MapToValue(reply, ([labels, _metadata, samples]) => {
return {
labels,
samples: transformMultiAggregationSamplesReply[3](samples)
};
});
}
},
} as const satisfies Command;
Loading
Loading