Skip to content

Add Redis TimeSeries support - #3978

Merged
a-TODO-rov merged 37 commits into
mainfrom
timeseries
Oct 26, 2026
Merged

a-TODO-rov merged 37 commits into
mainfrom
timeseries

Conversation

@a-TODO-rov

@a-TODO-rov a-TODO-rov commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds native Redis TimeSeries support: all 21 TS.* commands, through RedisTimeSeriesCommands and its async, reactive, Kotlin coroutine, cluster and node-selection flavors. It works on Redis 8.0 and later; TS.READ, TS.QUERYLABELS and TS.NRANGE/TS.NREVRANGE need Redis 8.10.

The branch has one commit per area, each reviewed as its own PR (#3967–#3972). Fixes from the final review follow as separate commits on top.

Commit Adds Reviewed in
Series lifecycle TS.CREATE, TS.ALTER, TS.DEL, TS.INFO; value types, series options, TsInfoValue, user guide #3967
Writes TS.ADD, TS.MADD, TS.INCRBY, TS.DECRBY; TS.MADD split by slot in Redis Cluster #3968
Compaction rules TS.CREATERULE, TS.DELETERULE #3969
Single-key reads TS.GET, TS.RANGE, TS.REVRANGE, TS.READ; range arguments, TsSample #3970
Label-index reads TS.MGET, TS.MRANGE, TS.MREVRANGE, TS.QUERYINDEX, TS.QUERYLABELS; TsFilter, GROUPBY/REDUCE #3971
Multi-key reads TS.NRANGE, TS.NREVRANGE; TsNRangeRow #3972

Key Decisions & Assumptions

  • Typed arguments per command: each command has its own args class. Options a command doesn't accept can't be set on it, and LABELS is always sent last.
  • Open value types: aggregation, reducer, duplicate policy and encoding are interface + enum + of(String). Values a newer server adds still pass through.
  • Version gates in Javadoc: several aggregations need Redis 8.8, NaN values 8.6, and the 8.10 commands and EXCLUDEEMPTY 8.10. The client rejects invalid combinations (for example ALIGN without an aggregation, or a bucket duration of 0 or less) before sending.
  • Redis Cluster:
    • TS.MADD is split by slot, keeping the caller's order. The split isn't atomic across slots.
    • The label-index commands run on one node, honoring ReadFrom.
    • Multi-key commands need all keys in one slot.
    • Read commands are registered read-only.
  • Partial failure: a rejected TS.MADD sample is a null element, also inside MULTI/EXEC (relies on fix(output): forward ByteBuffer error to queued command output (#3960) #3963).
  • Order: RESP3 labels keep the server's order (relies on Preserve server key order in MapComplexData (#3965) #3966).

Testing

  • Unit tests for the encoding of every command and option, and for parsing captured RESP2 and RESP3 replies.
  • Integration tests on Redis 8.0, 8.8, 8.10 and 8.12, standalone and Redis Cluster, through the sync, async, reactive and Kotlin APIs, plus RESP2/RESP3 parity tests.
  • The API consistency suite covers all flavors.

Notes


Note

Medium Risk
Large new command surface with non-trivial cluster routing (multi-slot MADD) and blocking TS.READ connection semantics; changes follow existing module patterns but affect production data paths.

Overview
Adds native Redis TimeSeries in Lettuce 8.0: all 21 TS.* commands surface through new RedisTimeSeriesCommands (and async, reactive, cluster, and node-selection APIs), wired via RedisTimeSeriesCommandBuilder, typed args/value types under io.lettuce.core.timeseries, and dedicated RESP parsers (TsSample, TsNRangeRow, error-tolerant TS.MADD list decoding).

Redis Cluster behavior is extended: TS.MADD is split per hash slot in parallel with caller order restored (not atomic across slots); label-index commands (TS.MGET, TS.MRANGE/MREVRANGE, TS.QUERYINDEX, TS.QUERYLABELS) route like other keyless reads via routeKeyless and honor ReadFrom.

Docs move TimeSeries from unsupported to Supported (8.0), add a user guide (timeseries.md), release notes, and mkdocs nav; the custom-command example now uses RedisGraph instead of TimeSeries.

Reviewed by Cursor Bugbot for commit 33b4c6b. Bugbot is set up for automated code reviews on this repo. Configure here.

a-TODO-rov and others added 6 commits October 22, 2026 11:26
* Add TimeSeries command types and keywords #2716

Add the TS.CREATE, TS.ALTER, TS.DEL and TS.INFO command types, the
keywords their options use, and register TS.INFO as read-only.

* Add TimeSeries value types #2716

Add TsAggregation, TsDuplicatePolicy and TsEncoding as interfaces with a
known-values enum each and a package-private raw value. of(String) looks
a name up by its wire token, ignoring case, and keeps an unknown name, so
that a value a newer server sends never breaks parsing.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TimeSeries series-option arguments #2716

Add TsCreateArgs and TsAlterArgs on package-private self-typed bases.
One final build method emits the options in a fixed order with LABELS
last, because the server stores every argument after LABELS as a label.
An empty label map sends a bare LABELS, which removes all labels on ALTER.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TsInfoValue and its parser #2716

TsInfoValue models the TS.INFO reply, with nested Rule and Chunk types
for the compaction rules and the DEBUG chunks. TsInfoValueParser reads
both RESP2 and RESP3 shapes into equal values, keeps unknown value-type
names as raw values, and exposes every reported field through rawInfo.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TimeSeries lifecycle commands to all API flavors #2716

Add TS.CREATE, TS.ALTER, TS.DEL and TS.INFO to the sync, async, reactive,
node-selection and Kotlin coroutines APIs, with a dedicated command
builder, and wire the new interfaces into the aggregate interfaces.
TS.DEL takes a Range<Long>; exclusive bounds are sent as the inclusive
neighbor, because timestamps are integer milliseconds.

The Kotlin flavor is part of this commit so that the coroutines
consistency test passes for the registered TIME_SERIES group.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TimeSeries user guide #2716

Add docs/user-guide/timeseries.md with the introduction, the supported
server versions, the value types and the series lifecycle commands, and
link it from the navigation.

* Add TimeSeries series lifecycle unit tests #2716

* Add TimeSeries series lifecycle integration tests #2716

* Add TimeSeries series lifecycle cluster tests #2716

* Return the wire token from TimeSeries value-type toString #2716

TsAggregationType, TsDuplicatePolicyType and TsEncodingType now return
the token sent to the server from toString(), for example STD.P
instead of the constant name STD_P. Unit tests check every constant.

---------

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add TimeSeries write command types and keywords #2716

Add the TS.ADD, TS.MADD, TS.INCRBY and TS.DECRBY command types and the
ON_DUPLICATE and TIMESTAMP keywords.

* Add TsAddArgs and TsIncrByArgs #2716

Add the TS.ADD arguments with the per-call ON_DUPLICATE override, and
the TS.INCRBY/TS.DECRBY arguments with TIMESTAMP, on the shared series
options. The base serializer now places TIMESTAMP first and
ON_DUPLICATE after DUPLICATE_POLICY, keeping LABELS last.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TsMAddValue #2716

TsMAddValue is one TS.MADD sample. of(key, timestamp, value) requires a
non-negative timestamp; of(key, value) sends * so that the server clock
sets the timestamp.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add error-tolerant integer list outputs #2716

TS.MADD replies with an array that holds an error element for each
rejected sample. ErrorTolerantIntegerListOutput turns those elements
into null and ErrorTolerantIntegerValueListOutput into Value.empty(),
reading the reply to the end; an error reply instead of the array still
fails the command.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TimeSeries write commands to all API flavors #2716

Add TS.ADD, TS.MADD, TS.INCRBY and TS.DECRBY to every API flavor. The
TS.ADD overloads without a timestamp send * (server clock); their
Javadoc warns that a long passed as the value compiles silently. TS.MADD
returns null for each rejected sample (Value.empty() on the reactive
API, List<Long?> on Kotlin), so the reactive tsMAdd and the builder
method tsMAddValues are registered as known deviations.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Split TS.MADD by slot in Redis Cluster #2716

The advanced cluster API sends TS.MADD with keys in several slots as one
command per slot, in parallel, and puts the results back in the caller's
order. Samples are partitioned by position, because one key can appear
several times. A slot whose command fails fails the whole call, as with
MGET. The split is not atomic across slots.

* Document TimeSeries writes #2716

Add the writes section to the TimeSeries user guide: TS.ADD with and
without a timestamp and the long-to-double pitfall, IGNORE (only with
duplicate policy LAST), TS.MADD partial failure and the per-slot split
in Redis Cluster, and TS.INCRBY/TS.DECRBY.

* Add TimeSeries writes unit tests #2716

* Add TimeSeries writes integration tests #2716

* Add TimeSeries writes cluster tests #2716

* Fix the Kotlin KDoc of tsAdd with the server clock #2716

The Java long-to-double warning does not apply in Kotlin, which does
not widen a Long to a Double. Drop it from tsAdd(key, value) and
tsAdd(key, value, args), and name the overloads that set the timestamp
as tsAdd(key, timestamp, value) and tsAdd(key, timestamp, value, args).

* Clarify where TS.MADD is split by slot #2716

The slot split happens in the Redis Cluster API only. Name it in the
tsMAdd documentation of all six flavors, and state in the class
documentation of both node-selection interfaces that tsMAdd runs
unchanged on every selected node, so its keys must hash to slots that
node serves.

---------

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add TimeSeries compaction rule commands #2716

Add TS.CREATERULE, with and without an alignment timestamp, and
TS.DELETERULE to every API flavor. The rule takes one TsAggregation, so
an aggregation this Lettuce version does not know can be passed with
TsAggregation.of(String).

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Document TimeSeries compaction rules #2716

Add the compaction rules section to the TimeSeries user guide, including
the requirement that source and destination share a slot in Redis
Cluster.

* Add TimeSeries compaction rule unit tests #2716

* Add TimeSeries compaction rule integration tests #2716

* Add TimeSeries compaction rule cluster tests #2716

* Fix cross-slot and alignment notes of TimeSeries rules #2716

Keys in different slots get CROSSSLOT only from Redis 8.4 on; Redis 8.0
and 8.2 reply "TSDB: the key does not exist" (TS.CREATERULE) or "TSDB:
compaction rule does not exist" (TS.DELETERULE). State both in the
Javadoc and the user guide. Reject a negative alignTimestamp with
IllegalArgumentException before sending, which the server would refuse.

* Add TimeSeries negative alignment tests #2716

* Reject a non-positive bucket duration in TS.CREATERULE #2716

The server rejects a bucket duration of zero or less. Check it in the
client and throw IllegalArgumentException before sending, and state it in
the tsCreateRule Javadoc of every flavor. The compaction-rule integration
test now expects the client-side IllegalArgumentException, and a builder
unit test covers 0 and -1 with and without alignment.

* Document the bucket duration check of TimeSeries rules #2716

State in the compaction-rule section of the user guide that the bucket
duration must be greater than 0 and that the client rejects other values
before sending the command.

---------

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add TimeSeries read command types and keywords #2716

Add the TS.GET, TS.RANGE, TS.REVRANGE and TS.READ command types, the
keywords of the range and read options, and register the four commands
as read-only.

* Add TsSample and sample outputs #2716

TsSample is a timestamp with one or more values, compared with
Double.compare semantics so that NaN equals NaN. TsSampleOutput reads a
single sample or an empty reply (null), and TsSampleListOutput streams
each sample row as it is decoded. Both read RESP2 string values (NaN,
Infinity, -Infinity) and RESP3 doubles.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TimeSeries range arguments #2716

Add TsBucketTimestamp, the package-private BaseTsRangeArgs with the
options shared by every range command, and TsRangeArgs for TS.RANGE and
TS.REVRANGE. An aggregation takes one or more TsAggregation values, sent
as one comma-joined argument. ALIGN, BUCKETTIMESTAMP and EMPTY without
an aggregation are rejected when the arguments are built, because the
server ignores the last two silently.

* Add TS.GET, TS.RANGE and TS.REVRANGE to all API flavors #2716

TS.GET returns the latest sample or null (an empty Mono on the reactive
API); tsGet(key, true) sends LATEST. TS.RANGE and TS.REVRANGE take a
Range<Long> with the same bound rules as TS.DEL and optional
TsRangeArgs, and stream the samples to the reactive subscriber.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TS.READ #2716

Add TS.READ to every API flavor, with TsReadOffset for the starting point
(earliest, latest, new samples or a timestamp) and TsReadArgs for BLOCK
and MAX_COUNT. The Javadoc covers how BLOCK occupies the connection,
the dedicated connection it needs, Redis Cluster and replicas, and the
empty reply when the key is removed. Requires Redis 8.10.

* Document TimeSeries single-key reads #2716

Add the single-key reads section to the TimeSeries user guide: TS.GET
and LATEST, ranges with exclusive bounds, the range options and several
aggregations, and TS.READ with BLOCK, including the dedicated connection
it needs, Redis Cluster and replicas.

* Add TimeSeries single-key read unit tests #2716

* Add TimeSeries single-key read integration tests #2716

* Add TimeSeries single-key read cluster tests #2716

* Reject a non-positive bucket duration in range arguments #2716

The server rejects a bucket duration of zero or less. The range
arguments now check it when the aggregation is set and throw
IllegalArgumentException; a Duration shorter than one millisecond counts
as zero. The Duration unit test now expects zero to be rejected.

* Drop the NaN note from the TS.GET return description #2716

The TsSample class documentation already says that values may be NaN,
so the @return of tsGet(K) in all six flavors no longer repeats it.

* List the exact @throws of TsRangeArgs.Builder.aggregation #2716

The @throws of Builder.aggregation(Duration, TsAggregation...) now lists
the same conditions as the setter it delegates to.

* Document the timeout and NaN rules of TimeSeries reads #2716

The @throws of TsReadArgs.Builder.block(Duration, long) now lists the
exact timeout conditions of the setter, and the TsSample class
documentation says that values may be NaN on Redis 8.6 and later.

* Fix TimeSeries test compilation on JDK 9 and later #2716

assertThat((List<?>) …).containsExactly(…) does not compile on JDK 9
and later because of the wildcard capture. Read the transaction results
with an explicit type argument instead.

---------

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add label-index command types and keywords #2716

Add the TS.MGET, TS.MRANGE, TS.MREVRANGE, TS.QUERYINDEX and
TS.QUERYLABELS command types, the WITHLABELS, SELECTED_LABELS and
EXCLUDEEMPTY keywords, and register the five commands as read-only.

* Add TsFilter #2716

TsFilter builds one label filter expression for the label-index
commands: equal, notEqual, exists, notExists, in and notIn, plus raw for
syntax the factories do not cover. Its toString is the expression sent
to the server.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TS.MGET and TS.QUERYINDEX #2716

Add TS.MGET with TsMGetArgs (LATEST, WITHLABELS or SELECTED_LABELS) and
TS.QUERYINDEX to every API flavor, both taking TsFilter expressions.
TsMGetValueParser reads the RESP2 array and the RESP3 map into equal
TsMGetValue lists; TsSampleParser decodes the nested samples.

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>

* Add TsReducer and TsMRangeArgs #2716

Add TsReducer with the TsReducerType constants, and TsMRangeArgs for
TS.MRANGE and TS.MREVRANGE: the range options plus labels, GROUPBY with
a reducer and EXCLUDEEMPTY. build(CommandArgs, List<TsFilter>) places
FILTER between the range options and GROUPBY. Conflicts the server would
reject are rejected by the setter that completes them and again when the
arguments are built. The getters expose what the reply parser needs.

* Add TS.MRANGE and TS.MREVRANGE #2716

Add TS.MRANGE and TS.MREVRANGE to every API flavor, with TsFilter
expressions and optional TsMRangeArgs. TsSeriesParser reads the RESP2
array and the RESP3 map into TsSeries; it takes the aggregations, the
reducer and the GROUPBY flag from the request, so both protocols give
equal results, and it moves the RESP2 __source__ label into the group's
source keys. The whole reply is held in memory before parsing.

* Add TS.QUERYLABELS #2716

Add tsQueryLabels (LABELS) and tsQueryLabelValues (VALUES label) to every
API flavor. The overloads without a filter query all indexed series; the
filter overloads require at least one filter, so an empty array is a
usage error rather than an all-series query. Results are UTF-8 strings
whatever the codec. Requires Redis 8.10.

* Route keyless TimeSeries commands in Redis Cluster #2716

The advanced cluster API runs TS.MGET, TS.MRANGE, TS.MREVRANGE,
TS.QUERYINDEX and TS.QUERYLABELS on one node, chosen as for FT.*, with
routeKeyless (async) and routeKeylessMany (reactive). The commands are
read-only, so the node follows ReadFrom. There is no client fan-out,
because the server gathers across shards. The node-selection interfaces
document that each selected node returns every shard's series on 8.10.

* Document TimeSeries label-index reads #2716

Add the label-index reads section to the TimeSeries user guide: filters,
TS.MGET and TS.QUERYINDEX, TS.MRANGE with labels, GROUPBY and
EXCLUDEEMPTY, the in-memory reply limit, TS.QUERYLABELS, and the Redis
Cluster behavior by server version, including the error fallback.

* Add TimeSeries label-index read unit tests #2716

* Add TimeSeries label-index read integration tests #2716

* Add TimeSeries label-index read cluster tests #2716

* Disable the cluster async keyless argument test #2716

On the cluster async API, routeKeyless builds keyless commands inside
thenCompose after picking a node, so an invalid argument fails the
returned future instead of throwing from the call. Keep the test
disabled until #3962 is fixed for the keyless cluster commands.

* Document EXCLUDEEMPTY on servers before Redis 8.10 #2716

Servers before 8.10 accept EXCLUDEEMPTY and ignore it without an error,
so series without samples in the range are still returned. State this in
the Javadoc of excludeEmpty() and of TS.MRANGE/TS.MREVRANGE in every
flavor, and in the user guide.

* Document the bucket duration check of TsMRangeArgs #2716

TsMRangeArgs inherits the check that rejects a non-positive bucket
duration. State it in the Javadoc of its aggregation methods and Builder
factories, and add a unit test for the inherited check.

* Return the wire token from TsReducerType toString #2716

TsReducerType now returns the token sent to the server from toString(),
for example STD.P instead of the constant name STD_P, like the other
TimeSeries value types. A unit test checks every constant.

* Drop the REFRESHCLUSTER note from TS.QUERYLABELS #2716

TS.QUERYLABELS requires Redis 8.10, so the cluster note of the six
QUERYLABELS methods no longer mentions TIMESERIES.REFRESHCLUSTER on
Redis 8.0 to 8.8. All six flavors.

* List the exact @throws of TsMRangeArgs.Builder.aggregation #2716

The @throws of Builder.aggregation(Duration, TsAggregation...) now lists
the conditions of the setter it delegates to that can occur on new
arguments.

---------

Co-authored-by: HwangRock <peter9244@catholic.ac.kr>
* Add multi-key range command types #2716

Add the TS.NRANGE and TS.NREVRANGE command types and register both as
read-only.

* Add TsNRangeArgs and TsNRangeRow #2716

TsNRangeArgs holds the TS.NRANGE and TS.NREVRANGE options: the shared
range options and an aggregation with one aggregation list per key, or
aggregationForEachKey to send the same aggregations for every key. It is
built with the key count, so a list count that differs from the number
of keys is rejected. TsNRangeRow is one row of flat values with per-key
slices, and TsNRangeRowListOutput streams the rows.

* Add TS.NRANGE and TS.NREVRANGE #2716

Add TS.NRANGE and TS.NREVRANGE to every API flavor, with a key list or
two keys, a Range<Long> and optional TsNRangeArgs. The builder sends
numkeys and then the keys, so the first key routes the command in Redis
Cluster, and builds the output with the value count of each key. Rows
keep the server's order. Requires Redis 8.10.

* Document TimeSeries multi-key reads #2716

Add the multi-key reads section to the TimeSeries user guide (TS.NRANGE
and TS.NREVRANGE, per-key aggregation, one slot in Redis Cluster), the
Lettuce 8.0 entry in docs/new-features.md, and list RedisTimeSeries as
supported in docs/compatibility-roadmap.md.

* Add TimeSeries multi-key read unit tests #2716

* Add TimeSeries multi-key read integration tests #2716

* Add TimeSeries multi-key read cluster tests #2716

* Reject a non-positive bucket duration in TsNRangeArgs #2716

The server rejects a bucket duration of zero or less. aggregation and
aggregationForEachKey now throw IllegalArgumentException for it; their
Duration overloads already reject durations shorter than one
millisecond. State it in the Javadoc and add a unit test for the check.

* List the exact @throws of TsNRangeArgs duration aggregations #2716

The @throws of the Duration forms of Builder.aggregation,
Builder.aggregationForEachKey and aggregation(Duration, List) now list
the same conditions as their long counterparts, with the Duration
rules.
@a-TODO-rov a-TODO-rov changed the title Timeseries Add Redis TimeSeries support Oct 22, 2026
@a-TODO-rov
a-TODO-rov marked this pull request as ready for review October 22, 2026 08:29
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-26T15:09:53.572750Z 33b4c6b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3197184f15

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/io/lettuce/core/timeseries/TsInfoValue.java
BaseTsSeriesArgs no longer holds TIMESTAMP, ENCODING and ON_DUPLICATE.
Its final build calls the package-private hook buildOwnArgs first, then
emits the shared options with LABELS last. BaseTsCreateArgs is removed:
TsCreateArgs, TsAddArgs and TsIncrByArgs extend BaseTsSeriesArgs and
each keeps its own encoding with an encoding(TsEncoding) setter.
TsAddArgs writes ENCODING and ON_DUPLICATE, TsIncrByArgs TIMESTAMP and
ENCODING, TsAlterArgs nothing. The unit tests expect the new token order
and the new hierarchy.
Review finding 1.1.
TsInfoValueParser.parse returns null for a null reply instead of
throwing, so a TS.INFO queued in an EXEC aborted by WATCH completes
cleanly. Updated test: TsInfoValueParserUnitTests.nullDataThrows, now
nullDataGivesNull.


Review finding 1.2.
The TsFilter factories reject a label name or value containing
whitespace, '=', '!', '(', ')', ',' or a quote, which the server would
read as filter syntax; the message points to TsFilter.raw(String).
Review finding 1.3, with N2.
TsAggregation.of, TsReducer.of, TsDuplicatePolicy.of and TsEncoding.of
reject a name containing ',' or whitespace, which would break the wire
form (min,max) or become a raw value. The Javadoc of the four interfaces
says they are implemented by Lettuce, not by applications.
Review finding 1.9.
The public TsNRangeArgs.getColumnCounts(int), needed only by the command
builder, is removed. build(CommandArgs, int) returns the unmodifiable
column counts instead, and RedisTimeSeriesCommandBuilder uses them.
Review finding 1.6.
TsRangeArgs.Builder, TsMRangeArgs.Builder and TsNRangeArgs.Builder lose
empty(), align(long), alignStart(), alignEnd() and bucketTimestamp(...),
which always fail when used alone; the instance methods stay. Updated
tests: TsRangeArgsUnitTests (aggregationOnlyOptionsWithoutAggregation,
aggregationSetAfterDependentOptions, setterValidation, builderFactories),
TsNRangeArgsUnitTests (aggregationOnlyOptionsWithoutAggregation,
builderFactories).
Review finding 2.2.
The TsAlterArgs class Javadoc and Builder.label/labels say that on
TS.ALTER both send the complete label set, so labels not listed are
removed. Javadoc only.
Review finding 3.1.
docs/new-features.md said all 21 TS.* commands work on Redis 8.0; it now
says that TS.READ, TS.QUERYLABELS, TS.NRANGE and TS.NREVRANGE need
Redis 8.10.
Review finding 3.2.
The aligned tsCreateRule example in the user guide now creates its
destination series {sensor1}:max-1h first, as the rule requires.
Review finding 3.3.
The TsInfoValue, Rule and Chunk getters, the four of(String) factories
and the public constructors of TsInfoValueParser, TsMGetValueParser and
TsSeriesParser get @SInCE 8.0. Javadoc only.
Review finding 3.5.
Test Javadoc no longer points to documents that are not in the
repository (the canonical server notes, design and test-plan sections,
QA reports, HLD and requirement IDs). Scenario IDs and facts stay.
Comments only.
Review finding 1.4.
78d2fb3 changed the TS.ADD option order to ENCODING, ON_DUPLICATE,
then the shared options, but two A02b test Javadocs still listed the old
order. Comments only.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd03cd7ae4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/main/java/io/lettuce/core/output/TsSampleOutput.java
Comment on lines +163 to +165
for (String value : values) {
assertNoSyntax(value, "Value");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject empty members in membership filters

For inputs such as TsFilter.in("area", "") or an empty member among several values, this loop accepts the empty string because it contains no syntax characters, and the factory emits area=() or a list with an empty item. That no longer represents the requested value membership and is rejected or interpreted as malformed filter syntax by Redis, whereas the single-value factories already reject empty values immediately. Validate each member with the same non-empty check used by value(String).

Useful? React with 👍 / 👎.

Review finding 3.3 (the @author part, decided by the user).
Every new TimeSeries type, in main and test sources, gets
@author Aleksandar Todorov in its type Javadoc. Javadoc only.
User decision (after the batch), following RediSearch.
The TimeSeries command interfaces in every flavor, the Kotlin interface
and impl, and TsInfoValueParser, TsMGetValueParser and TsSeriesParser
take only the key type K; the parsers accept RedisCodec<K, ?>. The
aggregates extend the TimeSeries groups with <K>. Updated declarations
in RedisTimeSeriesIntegrationTests and RedisTimeSeriesClusterIntegrationTests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ed5498e47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


@Override
public RedisFuture<List<TsMGetValue<K>>> tsMGet(TsFilter filter) {
return routeKeyless(() -> super.tsMGet(filter), conn -> conn.tsMGet(filter), CommandType.TS_MGET);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate routed TimeSeries command errors

When the selected node executes this command but returns a server-side error—for example NOPERM, an unsupported command during a rolling upgrade, or a malformed raw filter—routeKeyless treats the exceptional completion as a routing failure and invokes superCall, issuing the read again through the default cluster writer. This can mask the original error or return data from a node that does not follow the configured ReadFrom policy; the added keyless TimeSeries overrides should fall back only when connection selection fails and propagate command execution errors.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decision D7.3 (ts-docs/DECISIONS-2026-10-26.md); review status §4 items 4 and 5.
Source: review status §4.

count(n) on TsRangeArgs, TsMRangeArgs and TsNRangeArgs (and their Builder
factories) rejects n <= 0 with IllegalArgumentException; the server
answers "Invalid COUNT value" for both (checked on 8.10). A bucket
Duration longer than Long.MAX_VALUE milliseconds now throws
IllegalArgumentException ("Bucket duration is too large") from
TsArgsSupport instead of ArithmeticException.

The ALIGN check with an open range end is not in this commit: the args
don't see the range, so a client check needs an API decision (see
ts-run/STATUS.md).
Decision D7.5 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 2.
Source: review status §4.

The per-key aggregation(long|Duration, ...) setters and their Builder
factories take List<? extends List<? extends TsAggregation>>, so a
prebuilt list of TsAggregationType lists compiles. The copy and the
emitted tokens are unchanged. Source-compatible widening.
Decision D1 (ts-docs/DECISIONS-2026-10-26.md); review status §1 item 1.
Source: review status §1 item 1.

tsAdd(key, value[, args]) and TsMAddValue.of(key, value) are renamed to
tsAddNow and TsMAddValue.now in all flavors (sync, async, reactive,
node selection, Kotlin), so a lone long timestamp no longer widens to
the value-only overload. The widening warning is dropped from the
Javadoc and the user guide, whose examples use the new names.

Renamed calls in RedisTimeSeriesCommandBuilderUnitTests (A05),
RedisTimeSeriesIntegrationTests (B13, null-key check) and
TsMAddValueUnitTests (U14, equality).
Decision D2 (ts-docs/DECISIONS-2026-10-26.md); review status §1 item 2.
Source: review status §1 item 2.

TsReadOffset.latest() becomes newest(); it still sends "+". The range
and MGET args keep latest(), which sends the server keyword LATEST, so
the two meanings no longer share a name. The TS.READ Javadoc in every
flavor, the TsReadOffset Javadoc and the user guide use the new name.

Renamed calls in RedisTimeSeriesCommandBuilderUnitTests, the
readOffsets integration test and TsReadOffsetUnitTests (A11).
Decision D7.4 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 3.
Source: review status §4.

Their constructors are already package-private, so no code outside the
package can subclass them; final states it. TsApiConventionsUnitTests
gets an M04 check that the three types are final.
Decision D5.4 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 4.
Source: review status §2 item 4.

TsSeriesParser removed the __reducer__ and __source__ labels from every
MRANGE/MREVRANGE series, so a series' own labels with those names were
missing under WITHLABELS. It now removes them only from a grouped reply,
where RESP2 uses them for the reducer and the sources.

Tests: TsSeriesParserUnitTests (RESP2 and RESP3 ungrouped replies keep
both labels; the grouped tests are unchanged) and an MRANGE WITHLABELS
round-trip in RedisTimeSeriesIntegrationTests.
Decision D5.5 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 5.
Source: review status §2 item 5.
Revised after review: REVIEW-FIXES-2026-10-26.md R1.

An exclusive upper bound 0 became toTimestamp -1, which the server
rejects ("wrong toTimestamp"). An inverted interval is not safe either:
with AGGREGATION ... EMPTY, Redis 8.10 fills a bucket at 0 for it. Any
upper bound below 0 now throws IllegalArgumentException on the client,
on RANGE/REVRANGE, DEL, MRANGE/MREVRANGE and NRANGE/NREVRANGE; nothing
is sent and nothing is clamped. The @throws of every Range overload in
every flavor and the user guide say so.

Tests: builder tests for exclusive 0, including(-1) and 5..excluding(0)
on every command, with exclusive 1 and an upper bound of exactly 0
still sent as 0; the A06c assertion for an exclusive upper bound
Long.MIN_VALUE + 1 now expects the exception; an integration test for
RANGE (also with EMPTY), REVRANGE and DEL, with the sample at 0 kept.
Decision D5.6 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 6.
Source: review status §2 item 6.

The TsFilter class Javadoc and the guide showed raw("room=\"living
room\""), whose quotes become part of the value, so it matches nothing.
The example is now raw("room=living room"), and the Javadoc and the
guide say that each filter is one RESP argument, so a space needs no
quotes and quotes are literal. The guide row shows a real use of raw.

Tests: TsFilterUnitTests (raw keeps the space) and an integration test:
raw("room=living room") matches through QUERYINDEX and MGET, the quoted
form matches nothing. equal() still rejects the space (finding 1.2), so
the "through equal" case of DECISIONS is asserted as a client rejection.
Decision D5.7 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 7.
Source: Codex bot on #3978.

in("area", "") sent area=(), which silently matches nothing, and
in("area", "north", "") got a server parse error. in and notIn now
check each value like equal does and throw IllegalArgumentException for
an empty one; the @PARAM and @throws Javadoc say so.

Test: TsFilterUnitTests, "" alone and among other values for both.
Decision D5.9 (ts-docs/DECISIONS-2026-10-26.md); review status §2 item 9.
Source: Codex detailed reviewer (A1).

The reactive cluster tsMAdd merged the per-slot MADDs with
Flux.mergeSequential(publishers), which subscribes at most 256 at a
time. With auto-flush off and samples in more than 256 slots, one
flushCommands() sent only 256 and the call hung. The merge now
subscribes to all slots (concurrency = number of slots), so every
command is written before the flush. The result order is unchanged.

Test: RedisTimeSeriesClusterIntegrationTests, 300 slots, auto-flush
off, one flush, all results in caller order (times out without the
fix on 8.8).
Decision D7.1 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 8.
Source: lettuce-jedis re-review.

TsInfoValueParser built the chunk type, the duplicate policy and the
rule aggregation with the strict of(String) factories, which reject ','
and whitespace, so such a server value would throw while decoding and
close the connection. A package-private TsValueTypes now holds lenient
lookups (enum constant for a known name, raw value otherwise) that the
parser uses; the public of() validates and then delegates to them.
TsReducer.of is not used by any parser, so it is unchanged.

Test: TsInfoValueParserUnitTests, RESP2 and RESP3 replies with
"z std", "keep,last" and "avg,max" decode to raw values kept as given.
Decision D7.2 (ts-docs/DECISIONS-2026-10-26.md); review status §4 item 1.
Source: review status §4.

The public TsSeriesParser constructor kept the caller's aggregation
list, so changing it later changed results already returned, and it
accepted grouped = true with a null reducer. It now keeps an
unmodifiable copy and throws IllegalArgumentException for a grouped
parser without a reducer. The constructor Javadoc says so.

Tests: TsSeriesParserUnitTests for both.
- Warn that a cancelled reactive TS.READ stays blocked
- Add @SInCE 8.0 to the TimeSeries output constructors
- Use RedisGraph in the roadmap custom-command example
- Drop GROUPBY from the return of MRANGE without args
- Add the NaN note to tsAddNow
- Describe NaN on INCRBY and DECRBY as a server rule
- Note that several aggregations need Redis 8.8
- State which TS.READ BLOCK is rejected in MULTI
- State that node-selection tsMAdd keys share one slot
- Close the RESP2 TimeSeries test connection
- Widen the TS.READ blocking margins and pause polling
- Add unit tests for TimeSeriesSlotPartition
- Check that all 9 TimeSeries writes are not read-only
- Fail with the missing key in keyed TimeSeries results
- Gate the COUNTNAN/COUNTALL reducer test on Redis 8.8
@a-TODO-rov
a-TODO-rov merged commit ba5d2eb into main Oct 26, 2026
11 checks passed
@a-TODO-rov
a-TODO-rov deleted the timeseries branch October 26, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant