Skip to content

fix(diff): prune extension-managed state from plans (TimescaleDB, PostGIS) - #1

Merged
juanmicl merged 6 commits into
mainfrom
fix/prune-hypertable-and-extension-schemas
Sep 1, 2026
Merged

juanmicl merged 6 commits into
mainfrom
fix/prune-hypertable-and-extension-schemas

Conversation

@juanmicl

@juanmicl juanmicl commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • TimescaleDB auto-index pruning: the implicit per-hypertable time-column index (<hypertable>_<dimension>_idx) is pruned from DB-only plans. The prune map is keyed by qualified {schema}.{index} with a set of qualified hypertable owners, so heuristic name collisions across distinct hypertables (table a dim b_c vs table a_b dim c, both yielding a_b_c_idx) no longer leak one side as false (destructive) drift.
  • Implicit spatial index pruning (catalog-driven): single-column indexes named exactly <table>_<column>_idx on a geometry/geography column — created by geoalchemy2 (<0.18 or spatial_index=True) at table-create time — are pruned. Detection keys on the column type via pg_type catalogs, never the name alone; works without geoalchemy2 importable in the sqlpush process. Indexes declared in metadata are never affected.
  • Extension-owned schemas stay out of derived scope: namespaces owned by installed extensions (e.g. topology from postgis_topology, which ALTER DATABASEs itself onto the search_path at install time) never enter the scope derived from the live search_path. Explicit schemas= is never filtered; the default schema stays in scope even when extensions are relocated into it.
  • Reflection search_path pinned to the default schema only (when in scope): the unqualified reflection pass resolves names via pg_table_is_visible over the session search_path; the previous full-scope pin made non-default tables masquerade as default-schema tables, producing false destructive DROP TABLE and duplicate unqualified drops in mixed-schema scopes. Restore failures now invalidate the pooled connection (SET survives ROLLBACK) instead of potentially leaking the restricted path.
  • CI on the PostGIS-carrying HA image: both test jobs run timescale/timescaledb-ha:pg17-ts2.25 (tag verified on Docker Hub; exact pin matching dev: PG 17.9 / ts 2.25.2) so the spatial pruning tests actually execute in CI.

Motivation

Dogfooding sqlpush against a FastAPI + TimescaleDB + PostGIS app produced false drift: plans tried to drop extension-managed indexes, objects in extension-owned schemas, and emitted duplicate/unqualified drops when postgis_topology stretched the database search_path beyond the declared scope.

Verification

  • 89 passed, 1 xfailed against live timescaledb-ha:pg17 (PG 17.9 / ts 2.25.2 / PostGIS 3.6.2); DB-free subset skips cleanly without a DB
  • ruff check, ruff format --check, ty check all clean
  • Full-diff design review in 2 rounds (initial review found the mixed-scope blocker; incremental re-review after fixes). New regression tests pin each behavior: mixed-scope in-sync → zero ops; DB-only table → exactly one qualified drop; explicit schemas= never filtered.

Release surface

CHANGELOG updated under [Unreleased] (Added + Fixed). No linked issue — this fix originated from dogfooding; context lives in this PR.

False-drift sources reported by the drift check on a real
PostgreSQL/TimescaleDB/PostGIS schema, both pruned from live-server
catalogs (no hardcoded names):

- TimescaleDB's implicit time-column index (<hypertable>_<dimension>_idx,
  from timescaledb_information.hypertables) is skipped in the DB-only
  branch of include_object when both name and parent table match.
- Extension-owned namespaces (pg_extension JOIN pg_namespace, e.g.
  postgis_topology's topology) are excluded from the scope derived from
  the search_path — such extensions ALTER DATABASE themselves onto the
  search_path, so they would otherwise enter the scope uninvited.
- The reflection session's search_path is now pinned to the resolved
  scope (and restored afterwards): alembic's unqualified pass resolves
  names via pg_table_is_visible, which previously surfaced those same
  extension tables a second time, unqualified (one object, two drops).
- Explicit schemas= are never filtered; the default schema stays in
  scope even though extensions relocate into it.
Alembic's None-schema reflection pass resolves table names via
pg_table_is_visible over the session search_path, so pinning to the
full scope list made non-default tables masquerade as default-schema
tables in mixed scopes: false destructive DROP TABLE plus duplicate
unqualified drops. Pin to the default schema alone, and only when it
is a scope member (otherwise _make_include_name filters the None-pass
out before reflection and no pin or restore happens). Restore-side
hardening: a failed restore now invalidates the pooled connection
instead of recycling it with the restricted path.
Both service containers move from timescale/timescaledb:2.29.2-pg16
to the pinned timescale/timescaledb-ha:2.25.2-pg17 — the exact stack
dev runs (PG 17, timescaledb 2.25.2, PostGIS carried), so the spatial
and extension-scope tests are actually exercised in CI instead of
skipping. Pinning style unchanged (no floating tags).
@juanmicl
juanmicl merged commit f50e8dc into main Sep 1, 2026
7 checks passed
@juanmicl
juanmicl deleted the fix/prune-hypertable-and-extension-schemas branch September 1, 2026 11:34
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