fix(plugin-drizzle): keyset pagination drops the equality clause for Date columns - #1653
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@pothos/core
@pothos/plugin-add-graphql
@pothos/plugin-complexity
@pothos/plugin-dataloader
@pothos/plugin-directives
@pothos/plugin-drizzle
@pothos/plugin-errors
@pothos/plugin-example
@pothos/plugin-federation
@pothos/plugin-grafast
@pothos/plugin-mocks
@pothos/plugin-prisma
@pothos/plugin-prisma-utils
@pothos/plugin-relay
@pothos/plugin-scope-auth
@pothos/plugin-simple-objects
@pothos/plugin-smart-subscriptions
@pothos/plugin-sub-graph
@pothos/plugin-tracing
@pothos/plugin-validation
@pothos/plugin-with-input
@pothos/plugin-zod
@pothos/tracing-newrelic
@pothos/tracing-opentelemetry
@pothos/tracing-sentry
@pothos/tracing-xray
commit: |
hayes
force-pushed
the
fix/drizzle-keyset-date-equality
branch
from
August 21, 2026 01:47
b11ba5e to
736e7d1
Compare
…for equality
Paging past a cursor compares `created_at < $1 OR (created_at = $2 AND id
< $3)`. The equality half was written with drizzle's `{ column: value }`
shorthand, which drizzle reads as a nested filter whenever the value is an
object. A Date has no enumerable keys, so the clause compiled to nothing
and the filter collapsed to `created_at < $1 OR id < $2`: rows outside the
page come back, and rows inside it are skipped whenever two rows share a
timestamp.
Ordering by a timestamp reached this once compound cursors started
preserving Date values instead of stringifying them. Before that the
shorthand happened to receive a string, which is not an object, so drizzle
built the comparison correctly.
Naming the operator drops the shorthand entirely rather than special
casing Date, so bytea, array, and json columns cannot reach it either.
The tests compile the generated filter to SQL, since the shape of the
filter object is not what went wrong.
No changeset: the cursor tagging change that introduced this has not been
released, so there is nothing for users to read about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHUkipCNUgbk6gsm38dkAb
hayes
force-pushed
the
fix/drizzle-keyset-date-equality
branch
from
August 21, 2026 01:49
736e7d1 to
6c8bae4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow up to #1651, reported from a run against Postgres 17.11.
What breaks
Ordering a connection by a timestamp column returns the wrong rows. With 12 rows sharing a millisecond and
{ createdAt: 'desc', id: 'desc' },first: 3walks all 12 instead of 3.The keyset filter should be:
What actually reached Postgres:
Those are two unrelated conditions, not a keyset comparison. Rows outside the page come back, and a newer row with a smaller id is returned as though it were older.
Cause
The equality half was written with drizzle's
{ column: value }shorthand.relationsFieldFilterToSQLtreats any object value as a nested filter rather than a comparison:Object.entries(new Date())is[], so the clause returnsundefinedand disappears. No error.nullhit the same branch and threw, which is why it was already special cased;Datefails silently instead.This only became reachable when compound cursors started carrying tagged values. Before that, JSON serialization handed the filter a date string, and a string is not an object, so drizzle built the comparison correctly by accident. Single column orderings never emit an equality clause, and expression orderings build their SQL through
RAW, so neither path was affected.Fix
Name the operator instead of relying on the shorthand:
The shorthand is gone entirely rather than special cased for
Date, so bytea, array, and json columns cannot reach it either. Arrays would have crashed onoperators['0']rather than compiling away.Tests
Two tests in
cursor-values.test.tsbuild a filter throughdrizzleCursorConnectionQueryand compile it with.toSQL(). Asserting the shape of the filter object would have passed on the broken code, since the object looked right and drizzle discarded it. Verified red before the fix:The 16 unrelated test file failures in the drizzle suite are pre existing, from
@pothos/plugin-errorsresolving against an unbuilt workspace. Same count before and after this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01YHUkipCNUgbk6gsm38dkAb