Skip to content

bug(sdk): liveQuery dedup boundary is wrong — duplicate and dropped events in the core streaming feature #396

Description

@taitelee

Summary

LiveQuery dedupes buffered live events against the historical snapshot using rows[rows.length - 1].received_timestamp as the boundary. That boundary is wrong in the documented usage, and the column it relies on isn't even in the table.

Detail

clients/ts/src/stream/live-query.ts:63-81:

  • The canonical docs example orders received_timestamp DESC, so the last row is the oldest → boundary is minimal → every buffered event passes and rows already in the snapshot are re-delivered via next().
  • The envelope received_timestamp is never inserted into the table (internal/ingest/worker.go inserts only the inner payload), and .select(...) projections drop it anyway → lastTimestamp is undefined → no dedup at all, despite the "deduplicated automatically" docstring (query-builder.ts:180-188).
  • When the column does exist, envelope timestamps are RFC3339Nano while a second-precision row serializes as ...:05Z; lexicographically "...:05Z" > "...:05.123Z", so buffered events in the same second as the boundary are wrongly discarded — silent loss.
  • On fetch error (:58-61) buffered live events are dropped, never delivered.

Fix direction

Dedup on a stable server-provided key/sequence rather than the envelope timestamp string; use Math.max over the fetched rows' order column (not positional last), and compare with parsed instants, not lexically.

Found in a repo-wide audit; verified by code + docs. Distinct from #372 (data-column zone drift) and #98 (replay-vs-live labeling).

Activity

  1. added
    bugSomething isn't working
    area/sdkTypeScript SDK (clients/ts/)
    on Jul 8, 2026
  2. coderabbitai commented on Jul 8, 2026

    @coderabbitai
    🔗 Related PRs

    #174 - refactor(table names): handle unsafe table names [closed]
    #182 - refactor: full api --> ingest --> clickhouse --> dlq refactor [closed]
    #313 - feat(docs): view-transition polish, branded search + 404, chrome pass [closed]
    #364 - feat(dedupe): observe or reject ingest rows missing the id_field [merged]


    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  3. EricAndrechek commented on Aug 25, 2026

    @EricAndrechek
    Member

    Consolidating #449 into this issue — same bug, same lines (clients/ts/src/stream/live-query.ts:63-81). #449 was filed independently from the PR #434 pre-push docs review; CodeRabbit flagged the overlap on 2026-08-11. This issue is the survivor because it is the superset: #449's two failure modes (positional-last boundary, lexical string compare) are both here, plus the missing-column case, the sub-second-precision silent drop, and the fetch-error buffer drop.

    Porting the three things #449 had that this issue does not:

    1. There is a correct reference implementation — but it only fixes two of the three modes.

    clients/go/live_query.go:89-100 on the go-sdk branch (PR #434 — not on main, so this is a reference to read, not code to point users at yet) takes the max over parsed time.RFC3339Nano values across all rows, independent of ordering:

    var lastTS time.Time
    for _, row := range rows {
        if s, ok := row["received_timestamp"].(string); ok {
            if ts, perr := time.Parse(time.RFC3339Nano, s); perr == nil && ts.After(lastTS) {
                lastTS = ts
            }
        }
    }

    Worth being precise about the limit: that loop fixes the wrong-row and lexical-compare modes, but on a table with no received_timestamp column the type assertion never succeeds, lastTS stays zero, and the Go client silently skips dedup exactly like the TS one. Porting the Go approach to TS closes two of three modes and leaves the worst one open. Whatever lands should settle the missing-column question explicitly (require it and fail loudly / let the caller nominate the ordering column / scope the feature to ingest-API tables) rather than inheriting the silence.

    2. Three live references point at #449 and will need updating with the fix.

    Both docs lines are published on the public docs site. The prose at :188 was written to describe the bug and should revert to the plain "newest historical timestamp" description once this is fixed — that instruction lives only in #449 and is preserved here so it isn't lost behind a closed issue. The references themselves still resolve (a closed issue keeps its URL), but they should be repointed here or removed as part of the fix PR.

    3. A consumer already routed around the feature.

    From Eric on #449: a first-party browser app building a live-events view chose .stream() over .liveQuery() specifically because .liveQuery() couldn't be relied on for arbitrary user-defined tables. The feature was avoided rather than worked around — which is the strongest signal in either issue about what this costs, and does not otherwise appear on this one.

    Related: #449 (closed as duplicate of this), #434 (Go SDK, carries the reference implementation), #372, #98.


    Consolidated by the pm-triage routine on 2026-08-25; verified by code-read against b2eee9d and origin/go-sdk.

  4. EricAndrechek commented on Aug 25, 2026

    @EricAndrechek
    Member

    Priority raised P2 → P1, and #449 consolidated in (see above).

    Rationale: liveQuery() is a headline documented feature, this is live in the shipped public v0.1.0, and the consolidation added a datapoint that wasn't on either issue alone — a first-party consumer avoided the feature entirely rather than working around it. The docs currently describe the broken behaviour as the behaviour.

    This is under a re-anchored rubric (the old P0/P1 definitions were pinned to the launch, which shipped 08-19); the new anchor and all five changes are recorded in #501. Flagging rather than assuming — say so if you'd rate it differently.


    pm-triage routine, 2026-08-25, on Eric's instruction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/dedupeDeduplication (Pebble, ScyllaDB)area/sdkTypeScript SDK (clients/ts/)area/streamingSSE / live-query delivery path (/v1/stream)bugSomething isn't working

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions