Skip to content

fix: negate the day range in Timestamp != date filters#655

Open
TheSaiEaranti wants to merge 1 commit into
redis:mainfrom
TheSaiEaranti:fix-timestamp-ne-date
Open

fix: negate the day range in Timestamp != date filters#655
TheSaiEaranti wants to merge 1 commit into
redis:mainfrom
TheSaiEaranti:fix-timestamp-ne-date

Conversation

@TheSaiEaranti

@TheSaiEaranti TheSaiEaranti commented Jul 26, 2026

Copy link
Copy Markdown

What this fixes

Timestamp.__ne__'s docstring promises: "For date objects (without time), this excludes the entire day." The implementation called self.between(start, end) — the same positive range construction as __eq__ — so the filter matched the entire day instead:

Timestamp("created_at") == date(2023, 3, 17)   # @created_at:[1679029200.0 1679115599.999999]
Timestamp("created_at") != date(2023, 3, 17)   # @created_at:[1679011200.0 1679097599.999999]  ← positive range!

Silent inverted filtering: != with a date returns exactly the rows the caller asked to exclude, with no error or warning. The non-date path of the same method already negates correctly via FilterOperator.NE ((-@created_at:[v v])), as do the NE forms of Tag, Text, Num, and Geo — the date branch was the one exception.

There was a second, smaller asymmetry hiding in the same branch: __eq__ computes its day bounds with .astimezone(timezone.utc) while __ne__ used bare datetime.combine, so the two methods disagreed about where the day starts on machines whose local timezone isn't UTC.

The fix

Compute the day bounds exactly as __eq__ does (including the astimezone conversion), convert with _convert_to_timestamp exactly as between() does, and emit through the existing OPERATOR_MAP[FilterOperator.NE] template. The invariant this establishes: str(ts != d) is byte-for-byte f"(-{ts == d})" on any machine, for both date objects and date-only ISO strings ("2023-03-17"), which take the same branch. The datetime/unix path is untouched.

Tests

Added test_timestamp_not_equal_date, which asserts the negated-range form, the exact-negation-of-__eq__ invariant, and the ISO-string path. The existing suite covered != only with a datetime (the non-date path), which is why this never failed. The new test fails on current main and passes with the fix.

tests/unit/test_filter.py: 63/63 pass. Full tests/unit: 1168 passed, 1 skipped — with 6 errors identical on unmodified main in my environment (Docker-dependent CLI fixtures; no daemon locally). black, isort, and mypy clean.

Notes

  • Bug-fix, so this should carry the auto:patch label per CONTRIBUTING — I can't set labels as an outside contributor.
  • Found by reading filter.py while working on docs: add exception reference and error handling guide #654 (exception docs, still open) — happy to rebase either PR if they land in either order; they don't overlap.

Note

Medium Risk
Changes query semantics for a previously broken filter path; callers relying on the old (incorrect) behavior would see different results, but the fix aligns with documented behavior and other filter types.

Overview
Fixes Timestamp != date (and date-only ISO strings) so they exclude the full day instead of matching it. The date branch no longer calls between(); it builds the same UTC day bounds as ==, converts them with _convert_to_timestamp, and emits a negated Redis range via FilterOperator.NE—so str(ts != d) matches (-{ts == d}).

Adds test_timestamp_not_equal_date to lock in the negated query shape, the == / != pairing, and the ISO-string path.

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

Timestamp.__ne__'s date branch called self.between(start, end) — the same
positive range construction as __eq__ — so `Timestamp("f") != date(...)`
matched the entire day its docstring says it excludes, silently returning
exactly the rows the caller asked to filter out. The non-date path already
negates via FilterOperator.NE, as do the NE forms of Tag, Text, Num and Geo.

Compute the day bounds exactly as __eq__ does (including the astimezone
conversion the date branch of __ne__ was also missing) and emit them through
the existing NE operator template, making `!= date` the exact string negation
of `== date` on any machine. Date-only ISO strings take the same path.
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