Skip to content

feat(trail): Postgres-backed snapshot store and source for trail - #16

Open
codemug wants to merge 4 commits into
mainfrom
feat/snapshot-trail-packages
Open

feat(trail): Postgres-backed snapshot store and source for trail#16
codemug wants to merge 4 commits into
mainfrom
feat/snapshot-trail-packages

Conversation

@codemug

@codemug codemug commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Two new trail packages that let tracked views and facts live in a Postgres snapshot store, plus a reconstruction of the trail-mcp Dockerfile.

trail-snapshot-store — a ViewStore backend

Implements trail's five-method ViewStore ABC against Postgres. Registered as views_pg under the trail.providers entry-point group.

Frames are stored as parquet bytes rather than shredded into columns: trail reads and writes whole frames, so a column-per-export schema would buy nothing and would need a migration every time a model's exports change — which has already happened twice in this project.

write() upserts. Appending would silently double every tracked frame on the second build, and a doubled row count still looks plausible enough to go unnoticed.

trail-snapshot-source — facts as panels, with a changefeed

Exposes snapshot.facts as panel columns and implements changed_since(cursor), keyed on published_at. A restatement arrives as a new row with a later published_at, so it re-dirties exactly the cells it should — without that, a view would never pick a correction up.

This mirrors trail-fmp, which keys the same changefeed on filing date (trail_fmp/source.py:172). trail already has the incremental engine; these packages feed it rather than reimplementing it.

Two entry-point groups, deliberately

Package Group Why
trail-snapshot-store trail.providers writeable view stores (trail/providers.py:17)
trail-snapshot-source trail.sources read side, same as trail-fmp

A store declared under trail.sources resolves to nothing and trail silently falls back to a local-disk store — a deployment that looks healthy while writing views somewhere else entirely. Both groups are asserted in the tests.

Note also that trail/providers.py:store_for_config reads config.providers.get("views") — a hardcoded key. The trail.yaml stanza must be named views whatever the driver is.

DSN handling

Both packages take the connection string from options.dsn or the SNAPSHOT_DSN environment variable — the same precedence trail-fmp uses for FMP_API_KEY. trail.yaml is a ConfigMap kept in version control, so a password-bearing DSN cannot live in it.

Dockerfile reconstruction

deploy/Dockerfile.trail-mcp had drifted back to a pre-ghcr version: vendoring trail-py and re-adding trail-edgar, which was deliberately removed after it aborted a full 5,368-name build by raising CompanyNotFoundError on an unknown ticker.

The working version existed only as an uncommitted working-tree edit and was lost when local main was reset to origin/main (upstream untracked deploy/ in 1eb3b06). Reconstructed and verified against the running image before pushing — trail-lang 0.18.0, trail-fmp 0.3.0, trail-gmd 0.3.0, trail-scores 0.1.0, numpy 2.x, trail-edgar absent — and committed this time so it cannot be lost again.

Tests

trail-snapshot-store      6 passed
trail-snapshot-source     7 passed
aiofmp (tracked suite)  935 passed

Both suites run against a live Postgres carrying AGE + pgvector via SNAPSHOT_TEST_DSN.

Verified in the deployed cluster by mutation rather than status: renaming the view row in Postgres made trail report the new name, proving reads come from the database and not the stale /views PVC that would otherwise mask a silent fallback.

🤖 Generated with Claude Code

Tracked views land in the snapshot store rather than on local disk, so a view
sits alongside the facts it was computed from.

Frames are parquet bytes, not shredded columns: trail reads and writes whole
frames, so a column-per-export schema would need a migration every time a
model's exports change - which has happened twice already.

write() upserts. Appending would silently double every tracked frame on the
second build, and a doubled row count still looks plausible.

Registered under trail.providers (the writeable view-store group), NOT
trail.sources. Verified resolvable through trail's own resolver.
changed_since keys on published_at - the PIT clock - so a restatement, which
arrives as a NEW row with a later published_at, re-dirties exactly the cells it
should. Without that the view would never pick a correction up.

Mirrors trail-fmp, which keys the same changefeed on filing date. trail already
has the incremental engine; this feeds it rather than reimplementing it.

Registered under trail.sources, NOT trail.providers - verified separated.
trail.yaml is a ConfigMap that lives in version control, so a password-bearing
DSN cannot go in it. Options-first-then-environment is the precedence trail-fmp
already uses for FMP_API_KEY; this follows it rather than inventing a scheme.
…ckages

The working version of this file existed only as an uncommitted working-tree
edit and was lost when local main was reset to origin/main (upstream untracked
deploy/ in 1eb3b06). What remained on disk was the pre-ghcr version: vendoring
trail-py and re-adding trail-edgar, which was deliberately removed after it
aborted a full 5,368-name build on an unknown ticker.

Reconstructed and verified against the running cache-fix-1 image before
pushing: trail-lang 0.18.0, trail-fmp 0.3.0, trail-gmd 0.3.0, trail-scores
0.1.0, numpy 2.x, trail-edgar absent. Committed this time.

Adds trail-snapshot-store (trail.providers) and trail-snapshot-source
(trail.sources).
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