Repository navigation
⚡️ Only read the requested fields in getSnapshot() - #37
Conversation
Fixes #36 At the moment, `getSnapshot()` always selects the whole `data` column, and `fields ||= {}` can't tell "all fields" from "no fields". Since share/sharedb#770, every presence leave calls `getSnapshot()` with `fields: {}` just to read the version, so each one reads the whole document. This change stops reading `data` when `fields` is empty, so a live doc comes back with `data: {}`, as it does in [`sharedb-mongo`][1], and otherwise selects only the named keys. One consequence: a `json0` doc whose data isn't an object now projects to `{}` rather than `null` for empty `fields`, since checking its type would mean reading `data`. We leave `projectsSnapshots` false, so `Backend` still [re-projects][2] what we return, and the keyed projection passes non-object data through for it to turn into `null`. [1]: https://github.com/share/sharedb-mongo/blob/16c3fb60f69d9d5ddca6d7947e3bee9bf396ddbc/index.js#L1517-L1534 [2]: https://github.com/share/sharedb/blob/fb143054efe158337c56fc142716b1d43d91488c/lib/backend.js#L310-L312 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues block approval of the snapshot projection changes.
No open issues 🎉
What changed in this PR
Optimizes PostgreSQL snapshot reads for ShareDB field projections, avoiding document-data reads when callers only need version and type information, such as presence checks.
Changes:
- Distinguishes unrestricted reads from empty field selections and returns only requested keys for object data.
- Preserves full-data reads for
$submitand adds coverage for projection behavior, deleted and missing documents, and Backend integration.
| File | Description |
|---|---|
index.spec.js |
Adds snapshot projection tests and ShareDB Backend integration coverage. |
index.js |
Builds field-aware SQL to skip or project snapshot data as requested. |
Review effort: Balanced
| `SELECT version, ${dataColumn} AS data, doc_type, metadata FROM snapshots WHERE collection = $1 AND doc_id = $2 LIMIT 1`, | ||
| params, |
There was a problem hiding this comment.
Can't we use some query builder, to ensure no sql injection is possible
There was a problem hiding this comment.
No query builder needed here. Nothing from fields ever becomes SQL text: the keys only travel as the bound $3::text[] parameter, which pg sends separately from the query, so Postgres never parses them as SQL. (They also come from server-side projection config, not straight from clients.)
That was already true, but you had to trace selectData() to see it, so I've made it structural in 1d30448. The three possible statements are now plain string constants, with no ${…} anywhere in index.js. I've also added a test that asks for keys like x'::text[]); DROP TABLE snapshots; --, a,secret and NULL, and checks they come back verbatim without leaking anything else. It fails if keys are ever spliced into the SQL or hand-built into an array literal.
Refs #37 (comment) At the moment, `selectData()` assembles part of the `getSnapshot()` SQL at runtime, including the `$3` placeholder for the requested `fields` keys. The keys themselves are already bound, so this is safe, but checking that means tracing the helper. This change writes the three possible statements out as constants, so no runtime value can reach the SQL text, and adds a test with keys that would break the query, or leak other keys, if they were ever spliced in. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Fixes #36
At the moment,
getSnapshot()always selects the wholedatacolumn, andfields ||= {}can't tell "all fields" from "no fields". Since share/sharedb#770, every presence leave callsgetSnapshot()withfields: {}just to read the version, so each one reads the whole document.This change stops reading
datawhenfieldsis empty, so a live doc comes back withdata: {}, as it does insharedb-mongo, and otherwise selects only the named keys. One consequence: ajson0doc whose data isn't an object now projects to{}rather thannullfor emptyfields, since checking its type would mean readingdata.We leave
projectsSnapshotsfalse, soBackendstill re-projects what we return, and the keyed projection passes non-object data through for it to turn intonull.🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com