Skip to content

Refuse a filter value of the wrong type for a datetime column, and answer an invalid list filter with 422 (speedkick) - #1620

Merged
malatewang merged 4 commits into
MemMachine:speedkickfrom
edwinyyyu:fix/filter-range-text-speedkick
Sep 18, 2026
Merged

malatewang merged 4 commits into
MemMachine:speedkickfrom
edwinyyyu:fix/filter-range-text-speedkick

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1619.

Purpose of the change

A filter such as created_at > '2026-01-01T00:00:00Z' reached PostgreSQL as timestamp with time zone > character varying, failed there, and came back as HTTP 500 from /memories/list (episodic and semantic) and from /memories/search with types: ["semantic"]. The same filter ran on SQLite as a text comparison. SQLAlchemy types a bind by its Python value, so the mismatch was invisible until the database rejected it.

Three commits, one per concern:

  1. compile_sql_filter refuses the pairing before a statement is built. A column-encoded leaf pairs a DateTime column with a datetime value and nothing else; a str, an int or an IN list against created_at (or timestamp on the segment store), and a date() literal against a non-datetime column, raise ValueError with a message that names the field and points at date('...'). This holds on every SQL-backed store that resolves a datetime column: the episode store, the segment store and both semantic feature stores.
  2. list_memories maps ValueError to 422, as search_memories already did. Before, every invalid filter on list (a syntax error, an unknown field, this mismatch) was a 500.
  3. The filter field's description (SpecDoc.FILTER_MEM, and the two copies in docs/openapi.json) names date('...') as the form a datetime field compares with. Only those two descriptions change in the spec; a full local regeneration also drifts on ValidationError and info.version, which the main workflow owns.

The value is rejected rather than coerced: the filter language types a value by its literal, date() is the datetime literal, and a properties_json leaf already matches only a value of the declared type. Coercing a string here would make the column encoding disagree with the others.

Relation to #1616: that PR makes the parser reject a string in an ordering comparison, which covers >/</>=/<= with a str. This change is at the column boundary, so it also covers =, !=, an integer and an IN list, and it does not depend on the #1606 stack. The two compose; the rebase of either over the other is a few lines in _compile_column_leaf.

Tests

  • test_sql_filter_util.py: the fixture model gains a DateTime column; four mismatched filters raise with "holds a datetime", name > date(...) raises with "does not hold a datetime", and created_at > date(...) / >= date(...) compile and select the expected rows. test_bounds_are_normalized_to_utc's fixture column becomes a DateTime, which is what it models.
  • test_episode_storage.py: created_at > '...', created_at = '...' and created_at > 5 raise ValueError from both get_episode_messages and get_episode_messages_count, on SQLite and (under -m integration) PostgreSQL.
  • test_router.py: /memories/list answers a ValueError from list_search with 422.

uv run pytest packages/server/server_tests: 1937 passed, 3 skipped. -m integration on test_episode_storage.py (PostgreSQL via testcontainers): 24 passed. ruff check, ruff format --check, ty check --project packages/server clean.

End to end on a server backed by pgvector/pgvector:pg16 (pool_size: 2, max_overflow: 0): the text form on list (episodic, semantic) and on semantic search answers 422 with Field 'created_at' holds a datetime; compare it with date('...'), not str; nine of them in a row leave pg_stat_activity unchanged and the next valid request answers 200 in 4 ms. The same run on the unpatched tree answers 500 at each of those points. The connection leak in the original report did not reproduce on either tree; details in #1619.

main has the same parser, compiler and route, so it has the same defect; this PR targets speedkick per the current branch policy.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FNHipcrwW9JByUwhg3JVcj

edwinyyyu and others added 3 commits September 14, 2026 14:23
…the statement is built (speedkick)

A column-encoded leaf now pairs a DateTime column with a datetime value
and nothing else; any other pairing raises ValueError in
compile_sql_filter, on every SQL-backed store, with a message that
names the field and points at date('...').

SQLAlchemy types a bind by its Python value, not by the column, so
created_at > '2026-01-01T00:00:00Z' reached PostgreSQL as
timestamp with time zone > character varying and failed as a
ProgrammingError, which the routes answered with 500; on SQLite the
same statement ran as a text comparison. The reverse pairing, a date()
literal against a non-datetime column, failed the same way.

Tests: the compile-level cases (str, int and IN against a datetime
column; date() against a string column; the ordering that does
compile) and the episode store's created_at filter on both engines.
The normalization test's fixture column becomes a DateTime, which is
what it models.

Refs MemMachine#1619

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FNHipcrwW9JByUwhg3JVcj
…rch does (speedkick)

list_memories mapped no error to a status, so a FilterParseError, an
unknown field or a value of the wrong type was a 500 there and a 422
on search. The same ValueError -> 422 mapping now applies to both.

Refs MemMachine#1619

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FNHipcrwW9JByUwhg3JVcj
The filter description names date('...') as the form a datetime field
such as created_at compares with, and says a quoted string or a number
there is an invalid argument. docs/openapi.json carries the same two
descriptions; the rest of a local regeneration is drift the main
workflow owns.

Refs MemMachine#1619

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FNHipcrwW9JByUwhg3JVcj
@malatewang
malatewang merged commit 8d7b832 into MemMachine:speedkick Sep 18, 2026
41 checks passed
This was referenced Oct 7, 2026
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.

2 participants