Skip to content

feat(ingestion): add the transducer unique constraint and upsert loader - #847

Merged
jirhiker merged 1 commit into
stagingfrom
feat/transducer-upsert-loader
Aug 19, 2026
Merged

feat(ingestion): add the transducer unique constraint and upsert loader#847
jirhiker merged 1 commit into
stagingfrom
feat/transducer-upsert-loader

Conversation

@jirhiker

Copy link
Copy Markdown
Member

Task 3.4. Migration a1b2c3d4e5f6, loader in
automated_ingestion/ocotillo/loader.py, and three tests against a real
Postgres.

Why a constraint at all

transducer_observation had only an index on (deployment_id, parameter_id, observation_datetime), so nothing prevented inserting the same reading twice.
That absence is what forces a delete-then-repost load: with no constraint to
conflict on, a re-run can only avoid duplicates by removing what is already
there — which leaves a window where the data is missing, and if the run fails
midway, leaves it missing for good.

The plan named a column that does not exist

It called for UniqueConstraint(thing_id, parameter_id, observation_datetime).
thing_id is on TransducerObservationBlock, not on the observation table,
which carries deployment_id. The existing index already used deployment_id.

The scope is right on its own terms too: a deployment is a thing/sensor pairing,
so two sensors on one well may legitimately report the same instant.

Before merging — run the duplicate report

psql "..." -f automated_ingestion/sql/find_duplicate_observations.sql

The migration fails on a table that already violates the constraint, and
finding that out halfway through a production migration is worse than not
starting.

The report separates redundant copies from groups whose value disagrees. Those
are not duplicates but conflicting measurements, and collapsing them silently
would discard a reading somebody recorded — so it reports rather than deletes.

The local development database was clean (0 duplicate groups in 88,666 rows).
That is encouraging and is not evidence about production.

Details worth review

The migration drops the old index. The unique constraint creates an
equivalent one, and keeping both means two indexes maintained on every insert
into the largest table in the schema. Verified up and down against the 88,666-row
database.

DO UPDATE, not DO NOTHING. A vendor correction arriving as a no-op would
leave the old value in place while the run reported success — the worst of both
outcomes. There is a test.

Core, not ORM objects. AGENTS.md is explicit for high-volume tables:
instantiating a mapped class per observation is what turns a backfill into an
hour-long run.

Batches commit individually (5,000 rows). An interrupted load keeps what it
wrote, and resuming simply rewrites those rows because the write is an upsert.

Testing

The idempotency claim depends on Postgres enforcing the constraint and on
ON CONFLICT resolving against it, so it cannot be shown with a stub. All three
tests run against the test database. 49 ingestion tests still pass.

🤖 Generated with Claude Code

The table had only an index on (deployment_id, parameter_id,
observation_datetime), so nothing prevented inserting the same reading twice.
That absence is what forces a delete-then-repost load: with no constraint to
conflict on, a re-run can only avoid duplicates by removing what is there
first, leaving a window where the data is missing and, if the run fails
midway, leaving it missing for good.

The constraint is on deployment_id, not thing_id as the plan said -- that
column is on the block table, not this one. The existing index already used
deployment_id, and the scope is right anyway: a deployment is a thing/sensor
pairing, so two sensors on one well may legitimately report the same instant.

The migration drops the old index, since the constraint creates an equivalent
one and keeping both means maintaining two on every insert into the largest
table in the schema. Verified up and down against a database with 88,666 rows.

find_duplicate_observations.sql runs first: the migration fails on a table that
already violates the constraint. It separates redundant copies from groups
whose values disagree, because those are conflicting measurements rather than
duplicates and collapsing them would discard a reading somebody recorded.

The loader upserts with DO UPDATE rather than DO NOTHING, so a vendor
correction is applied instead of silently ignored, and uses Core rather than
ORM objects per AGENTS.md. The idempotency test runs against a real Postgres,
because the claim depends on the database enforcing the constraint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Coverage

79.40% total — gate is 75%.

Coverage for the Python files changed in this PR
Name Stmts Miss Cover Missing
automated_ingestion/ocotillo/loader.py 52 13 75% 60, 148-177
db/transducer.py 57 2 96% 98, 101
TOTAL 109 15 86%

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Your pull request is automatically being deployed to Dagster Cloud.

Location Status Link Updated
ocotillo-automated-ingestion View in Cloud Aug 19, 2026 at 06:06 AM (UTC)

@jirhiker
jirhiker merged commit 6f3232d into staging Aug 19, 2026
10 checks passed
@jirhiker
jirhiker deleted the feat/transducer-upsert-loader branch August 19, 2026 06:05
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