You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Three things the Embedded Sales Agent (scope3data/embedded-sales-agent) hit while adopting 9.0.0b2. Most of #1441 landed in b2; thank you. These are what came up using it.
1. replace_empty_legacy_reporting only takes an async psycopg connection
Most adopters run schema changes through a sync migration framework (Alembic on SQLAlchemy). To call the replacement from an Alembic revision, ESA opens a second, async psycopg connection and runs it with asyncio.run(...). That works, but the drop no longer commits atomically with the framework's version bump. If the revision fails after the replacement, the drop is already committed (harmless here, since the next run sees no legacy ledger, but it isn't what an operator expects from one migration). The asyncio.run also breaks if a migration runner is ever invoked from inside an event loop.
Ask: a sync variant that accepts a sync psycopg connection or a SQLAlchemy Connection, so the replacement runs inside the caller's transaction. Or at least a documented recipe for Alembic.
2. No rollback story for the owned ledger
Once create_schema() has built the caller-owned ledger, an 8.x image can't run against it. So rolling back past the 9.0 upgrade needs the owned ledger removed first. ESA's downgrade does this itself: it locks the tables, refuses if any holds a row, and drops them. That duplicates the care replace_empty_legacy_reporting already takes, in the other direction.
Ask: either a replace_empty_owned_reporting(connection) (same guarantees, the opposite shape) or a short rollback section in docs/reporting-caller-ownership-migration.md saying what an operator must do before deploying an 8.x image again.
3. Validating a ProductFormatDeclaration, then publishing a Format
ProductFormatDeclaration is now the strict authoring union, while Product.format_options is list[Format]. A seller converting stored declarations to products wants both: validate each option strictly, then publish it. Two obvious paths go wrong:
Passing the validated branch model to Product fails, because a ProductFormatDeclaration1 is not a Format.
Passing its model_dump(exclude_none=True) adds every branch default (asset_source, buyer_asset_acceptance, deprecated, experimental, …) to the published format, which changes the wire.
ESA now validates with validate_union(ProductFormatDeclaration, option) and publishes the original dict. Ask: document that pattern in the types guide, or add a helper that validates a declaration and returns the Format built from the caller's own fields (for example a model_dump(exclude_unset=True)-based projection).
code-reviewer: items 1 & 2 are non-breaking additions; sync psycopg 3 variant is cleanest for item 1; replace_empty_owned_reporting must mirror the advisory-lock → ACCESS EXCLUSIVE → row-guard → drop chain; guard adcp_reporting_ownership_archives/_backfills from the drop.
dx-expert: a recipe is insufficient — the reference Alembic env already occupies the event loop, so asyncio.run inside a revision raises at runtime; a function is required for item 2 because docs-only leaves operators writing unsafe DDL without the advisory lock or dependency guard.
My take: Items 1 & 2 have expert consensus and are non-breaking additions. Item 3 resolves once #1447 merges — ProductFormatDeclaration will subclass Format, making model_dump(exclude_unset=True) correct by construction; the pattern note belongs in #1447's already-updated migration docs.
Implementation scope (items 1–2):
Add replace_empty_legacy_reporting_sync(connection) in src/adcp/reporting/migration.py — sync psycopg 3, removes double-await, same isolation check and autocommit=True constraint; add Alembic usage note to docs/reporting-caller-ownership-migration.md.
Add replace_empty_owned_reporting(connection) — advisory lock → ACCESS EXCLUSIVE all reporting_*/adcp_reporting_* tables → refuse if any row → drop; inverse of ownership-marker check (line 116–119); skip adcp_reporting_ownership_archives/_backfills; add rollback section to docs.
Conformance tests in tests/conformance/reporting/test_reporting_empty_legacy_replacement.py (or a parallel file for the rollback path).
Both are non-breaking new exports; no existing signatures changed.
An update on point 3, after 9.0.0-beta.3. ProductFormatDeclaration is a Format subclass again, so passing it to Product works. But its model_validate now grades params from the schema. Stored declarations written before 9.0 can therefore stop projecting:
a slot with required_pixel_ratios but no pixel_ratios (correctly refused by the 3.2 rule);
a malformed params bag a seller chose to pass through.
ESA now publishes stored declarations through the open Format class, which matches what adcp 8 did, and keeps strict grading for authoring. Suggestion for the types guide: say plainly that Format is the lenient read and publish path, and ProductFormatDeclaration is the strict authoring check. Optionally add a non-raising validation_issues(data) so sellers can report data quality without failing a read.
Activity
bokelley commentedon Oct 8, 2026
Triage
Classification: Feature request (items 1–2); docs/ergonomics (item 3)
Bucket(s): client, docs
Status: ready-to-implement (items 1–2); deferred — blocked-on #1447 (item 3)
What the experts said:
replace_empty_owned_reportingmust mirror the advisory-lock → ACCESS EXCLUSIVE → row-guard → drop chain; guardadcp_reporting_ownership_archives/_backfillsfrom the drop.asyncio.runinside a revision raises at runtime; a function is required for item 2 because docs-only leaves operators writing unsafe DDL without the advisory lock or dependency guard.My take: Items 1 & 2 have expert consensus and are non-breaking additions. Item 3 resolves once #1447 merges —
ProductFormatDeclarationwill subclassFormat, makingmodel_dump(exclude_unset=True)correct by construction; the pattern note belongs in #1447's already-updated migration docs.Implementation scope (items 1–2):
replace_empty_legacy_reporting_sync(connection)insrc/adcp/reporting/migration.py— sync psycopg 3, removes double-await, same isolation check andautocommit=Trueconstraint; add Alembic usage note todocs/reporting-caller-ownership-migration.md.replace_empty_owned_reporting(connection)— advisory lock → ACCESS EXCLUSIVE allreporting_*/adcp_reporting_*tables → refuse if any row → drop; inverse of ownership-marker check (line 116–119); skipadcp_reporting_ownership_archives/_backfills; add rollback section to docs.tests/conformance/reporting/test_reporting_empty_legacy_replacement.py(or a parallel file for the rollback path).Item 3 — blocked-on #1447. Resurfaces on merge.
Triaged by Claude Code. Session: https://claude.ai/code/session_01PH5fm5TsM8KpWqELseCZyT
Generated by Claude Code
docs(types): show how a validated declaration is published as a Format
docs(types): show how a validated declaration is published as a Format
fix(types)!: keep ProductFormatDeclaration a class, graded from its s…
bokelley commentedon Oct 11, 2026
An update on point 3, after 9.0.0-beta.3.
ProductFormatDeclarationis aFormatsubclass again, so passing it toProductworks. But itsmodel_validatenow gradesparamsfrom the schema. Stored declarations written before 9.0 can therefore stop projecting:required_pixel_ratiosbut nopixel_ratios(correctly refused by the 3.2 rule);paramsbag a seller chose to pass through.ESA now publishes stored declarations through the open
Formatclass, which matches what adcp 8 did, and keeps strict grading for authoring. Suggestion for the types guide: say plainly thatFormatis the lenient read and publish path, andProductFormatDeclarationis the strict authoring check. Optionally add a non-raisingvalidation_issues(data)so sellers can report data quality without failing a read.