|
1 | | -import { and, eq, gt, or, type SQL } from 'drizzle-orm' |
| 1 | +import { and, gt, gte, lt, or, type SQL } from 'drizzle-orm' |
2 | 2 | import type { PgColumn } from 'drizzle-orm/pg-core' |
3 | 3 | import type { Cursor } from '@/lib/data-drains/types' |
4 | 4 |
|
@@ -29,17 +29,29 @@ export function decodeTimeCursor(cursor: Cursor): TimeCursor | null { |
29 | 29 |
|
30 | 30 | /** |
31 | 31 | * Builds a strict-greater-than predicate over a `(timestampCol, idCol)` pair. |
32 | | - * Equivalent to `(timestampCol, idCol) > (cursor.ts, cursor.id)` — rows on the |
33 | | - * cursor itself are excluded; ties on `timestampCol` break by `idCol`. |
| 32 | + * Semantically: `(timestampCol, idCol) > (cursor.ts, cursor.id)`. |
| 33 | + * |
| 34 | + * Postgres `timestamp` columns store microsecond precision but JS `Date` |
| 35 | + * round-trips at millisecond precision, so a row with PG value |
| 36 | + * `00:00:00.123456` survives the JS → cursor → JS round-trip as |
| 37 | + * `00:00:00.123000`. A naive `eq(col, new Date(cursor.ts))` would never match |
| 38 | + * such rows, and `gt(col, new Date(cursor.ts))` would match the cursor row |
| 39 | + * itself — re-emitting the last row of every chunk forever. |
| 40 | + * |
| 41 | + * Fix: compare in millisecond buckets. Anything in a strictly later ms bucket |
| 42 | + * is included; anything inside the cursor's own ms bucket is included only |
| 43 | + * when `idCol > cursor.id`. |
34 | 44 | */ |
35 | 45 | export function timeCursorPredicate( |
36 | 46 | timestampCol: PgColumn, |
37 | 47 | idCol: PgColumn, |
38 | 48 | cursor: TimeCursor | null |
39 | 49 | ): SQL | undefined { |
40 | 50 | if (!cursor) return undefined |
| 51 | + const cursorTs = new Date(cursor.ts) |
| 52 | + const nextMs = new Date(cursorTs.getTime() + 1) |
41 | 53 | return or( |
42 | | - gt(timestampCol, new Date(cursor.ts)), |
43 | | - and(eq(timestampCol, new Date(cursor.ts)), gt(idCol, cursor.id)) |
| 54 | + gte(timestampCol, nextMs), |
| 55 | + and(gte(timestampCol, cursorTs), lt(timestampCol, nextMs), gt(idCol, cursor.id)) |
44 | 56 | ) |
45 | 57 | } |
0 commit comments