Skip to content

join() shipped with zero cases in the generic tabular suite — the only shared block asserts which path ran, and the pushdown/fallback disagreement it missed shipped one day later #947

Description

@sroussey

What

5f83ae85f (feat(storage)!: add join() to ITabularStorage with SQL pushdown, #893) made join a required member of ITabularStorage (ITabularStorage.ts:796, no ?) with two implementations of the same semantics: the SQL pushdown in BaseSqlTabularStorage.runSqlJoin, and the hash fallback in BaseTabularStorage.join that every other backend inherits.

The shared contract got one block for it, and it asserts path selection, not results:

$ grep -n 'it(\|itImpl(' packages/test/src/contract/tabular-storage/assertions/joinPushdown.ts
61:      itImpl(   "runs a same-connection join without querying the right side"
78:      itImpl(   "falls back to the hash join for a right side on another connection"

Two cases. Both use the same spec — type: "left", one on pair, one orderBy, no where, no limit, no offset, no compound key, no null keys — and both assert rightQuery was/was not called plus a two-row shape.

The generic suite that every backend rides has nothing:

$ grep -c 'join' packages/test/src/test/storage-tabular/genericTabularStorageTests.ts
0

And joinPushdown is gated on createSiblingStorage, which only three backends supply:

$ grep -rl 'createSiblingStorage' packages/test/src/test/storage-tabular/
packages/test/src/test/storage-tabular/DuckDbTabularStorage.integration.test.ts
packages/test/src/test/storage-tabular/PostgresTabularStorage.integration.test.ts
packages/test/src/test/storage-tabular/SqliteTabularStorage.integration.test.ts

$ grep -rl 'runTabularStorageContract' packages/test/src/test/storage-tabular/ | wc -l
10

So seven of the ten backends that ride the tabular contract get no join assertion of any kind, and all seven use the hash fallback: InMemory, SharedInMemory, IndexedDb, Cached, Telemetry, FsFolder, Supabase.

Why it matters

The bug this predicts already happened, one day after the feature landed. 701589c30 (#934):

planSqlJoin sent a join to the hash fallback when the right-hand storage sat inside a connection transaction the left side was not enlisted in, by testing right.inTransaction. … on a real pg.Pool the guard was dead: the pushdown ran, read the right-hand table off a different pooled client, and missed the transaction's uncommitted rows. An inner join returned zero rows where the same call on SQLite, or through the hash fallback, returned the row — a wrong answer with no error, differing between backends and between the two strategies for identical inputs.

That is precisely the property the contract does not state. joinPushdown would have stayed green through it: its own fixture is not inside a transaction, and it never compares the two strategies' rows for one spec.

The interface docstring lists the semantics that differ and are unasserted anywhere shared (ITabularStorage.ts:409-796): where.right "applied as part of the join condition rather than after it, so under a left join an unmatched left row survives"; limit/offset "applied to the joined rows"; the hash join "bounds the left read — and stops early — only when the joined order does not need a sort"; "A join key that is null (or absent) on either side never matches, as in SQL." Four rules with two implementations each and no shared case.

Per-backend coverage exists and is good (45 co-located cases across JoinSql.test.ts 11, hashJoin.test.ts 27, joinDelegate.test.ts 7, plus SQLite/Postgres/Scoped/Cached integration cases) — but it tests each half in isolation. Nothing runs the same spec down both paths and compares.

Proposed fix

  1. Add a joinSemanticsBlock to runTabularStorageContract (so all ten backends run it), covering the four documented rules above: null key never matches, where.right under a left join, compound on, limit/offset applied to joined rows, orderBy across sides.
  2. Add the parity case the fallback/pushdown split needs and joinPushdown does not have: on a backend that supports both, run one non-trivial spec through the pushdown and through the fallback (same rows, sibling storage vs a deliberately foreign right side) and assert equal results, not just which path ran. guardParity.ts in the same directory is the template — it compares outcomes across two routes over a derived method set rather than listing them.
  3. InMemoryTabularStorage is the free reference implementation for the semantics half, the same way it is for the new vector contract (The new IVectorStorage contract runs on 6 of 8 implementations — the two left out are the one the KnowledgeBase uses on SQLite and the Supabase backend #941).

Measured on origin/main @ 2d36880 (0.6.0). bun scripts/test.ts vitest unit storage → 59 passed / 2 skipped of 63 files, 1176 tests (the two failures here are wall-clock timeouts on PGlite boot in a slow sandbox, not assertions).

Found during the 2026-09-14 review of packages/.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions