Skip to content

⚡️ Only read the requested fields in getSnapshot() - #37

Merged
alecgibson merged 2 commits into
mainfrom
worktree-snazzy-leaping-nebula
Nov 12, 2026
Merged

alecgibson merged 2 commits into
mainfrom
worktree-snazzy-leaping-nebula

Conversation

@alecgibson

Copy link
Copy Markdown
Collaborator

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, 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 what we return, and the keyed projection passes non-object data through for it to turn into null.

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 $submit and 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

@alecgibson
alecgibson marked this pull request as ready for review November 10, 2026 16:00
Comment thread index.js Outdated
Comment on lines +123 to +124
`SELECT version, ${dataColumn} AS data, doc_type, metadata FROM snapshots WHERE collection = $1 AND doc_id = $2 LIMIT 1`,
params,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use some query builder, to ensure no sql injection is possible

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@alecgibson
alecgibson merged commit 78f4417 into main Nov 12, 2026
15 checks passed
@alecgibson
alecgibson deleted the worktree-snazzy-leaping-nebula branch November 12, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getSnapshot() reads data even when fields asks for none

3 participants