Skip to content

feat(module-postgres): initial snapshot filters for Postgres sources - #754

Draft
henriquekraemer wants to merge 3 commits into
powersync-ja:mainfrom
henriquekraemer:feat/initial-snapshot-filters-postgres
Draft

feat(module-postgres): initial snapshot filters for Postgres sources#754
henriquekraemer wants to merge 3 commits into
powersync-ja:mainfrom
henriquekraemer:feat/initial-snapshot-filters-postgres

Conversation

@henriquekraemer

Copy link
Copy Markdown
Contributor

Background

This is the Postgres subset of the prefiltering work in #502, which we've been running in our staging environment (schema-per-tenant, 4 tenant schemas, largest table 13.6M rows). It follows the direction from that discussion: filters are global rather than per-definition, keyed by table patterns with the same wildcard support as bucket queries, and sql as the database-specific key so other syntaxes can be added later.

What this does

Adds a global initial_snapshot_filters section to the sync config, mapping table patterns (including schema and table wildcards) to a SQL WHERE clause applied during the initial snapshot:

initial_snapshot_filters:
  maint_order_activities:
    sql: "ps_metadata IS NOT NULL AND deleted_at IS NULL"
  "%":
    sql: "deleted_at IS NULL"

Matching is first-match-wins, so specific tables go before wildcard patterns. The filter is applied in all snapshot query types, including the chunked resume path and row re-fetches during streaming. Rows excluded by the filter are skipped by the source database instead of being read, evaluated and discarded by the service, and never enter current_data.

The second commit fixes an interaction with compiled sync plans: with config.edition: 3 the deploy persists a compiled plan, and replication rebuilds the config from the stored plan (parsePersistedSyncConfigContent), which did not restore initialSnapshotFilters. The filters parsed and validated fine but never reached the snapshot. They are now persisted alongside the plan (the same treatment eventDescriptors already get) as an ordered list of {pattern, filter} pairs, since matching is first-match-wins and JSONB storage does not preserve object key order. Plans stored before this field existed are treated as having no filters.

Results

On our largest tenant the initial snapshot of a 13.6M row table went from reading all rows to only the ~282k matching the filter (~2% working set), and the full multi-tenant snapshot completes in minutes instead of hours.

One operational note that may be worth a line in the docs: a selective filter on a big table needs an index matching the predicate, since the chunked snapshot query is WHERE <filter> ORDER BY pk LIMIT n. Without one, our first chunk on that table took 8m38s, which blows the snapshot socket timeout and leaves the table retrying. With a partial index on (id) using the same predicate, 2.6s.

Tests

  • module-postgres: filtered chunked snapshot with resume, a bare table name pattern matching the same table across multiple schemas, and an edition-3 test that reloads the config from storage before replicating, covering the persisted-plan path. That last test is also what caught the filters not being applied with MongoDB bucket storage: the other test queries filter the same rows as the snapshot filter, so they pass either way, while the edition-3 test selects all rows and lets the row count measure the filter.
  • service-core: round-trip test for filters through compiled plan persistence, including pattern ordering and plans persisted before the field existed.

Port of the Postgres-relevant subset of powersync-ja#502
onto current main, for testing against a Postgres source and Postgres
storage.

Adds a global initial_snapshot_filters section to the sync config yaml,
mapping table patterns (including schema and table wildcards) to a SQL
WHERE clause applied during the initial snapshot. The filter is applied
in all three snapshot query types, including the chunked resume path and
row re-fetches during streaming.

Includes Postgres tests, which the original PR does not have yet:
filtered chunked snapshot with resume, and a bare table name pattern
matching the same table across multiple schemas.
… sync plan

With config.edition: 3, deploying sync rules persists a compiled sync
plan, and replication restores the config from that plan instead of
re-parsing the YAML (parsePersistedSyncConfigContent). Initial snapshot
filters were not part of the serialized plan, so they were silently
dropped on that path: getInitialSnapshotFilter() always returned
undefined and initial snapshots read entire tables.

Store the filters alongside the plan - like event descriptors - as an
ordered [pattern, filter] list (matching is first-match-wins and JSONB
does not preserve object key order), and restore them when parsing a
persisted plan. Plans stored before this field existed are treated as
having no filters.

The new module-postgres regression test reloads the sync config from
storage before replicating (like the replicator does on startup) and
uses a stream that selects all rows, so the bucket row count directly
measures whether the filter reached the snapshot query.
@changeset-bot

changeset-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f515abc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@powersync/service-sync-rules Minor
@powersync/service-core Minor
@powersync/service-module-postgres Minor
@powersync/service-module-postgres-storage Patch
@powersync/service-module-mongodb-storage Patch
@powersync/service-jpgwire Patch
@powersync/service-core-tests Patch
@powersync/lib-services-framework Patch
@powersync/service-module-convex Patch
@powersync/service-module-mongodb Patch
@powersync/service-module-mssql Patch
@powersync/service-module-mysql Patch
@powersync/service-module-core Patch
@powersync/service-image Minor
test-client Patch
@powersync/service-schema Minor
@powersync/lib-service-postgres Patch
@powersync/service-rsocket-router Patch
@powersync/lib-service-mongodb Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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.

1 participant