Skip to content

ADR-017: Python Insert-Only Transaction Bridging

Status

Accepted

Context

ADR-006 chose AwaTransaction as a narrow sqlx-backed transaction surface for Python instead of trying to integrate directly with psycopg3, asyncpg, SQLAlchemy, or Django.

That decision rejected two things:

  • sharing a live database connection between different drivers
  • growing Awa into a general ORM abstraction

Those constraints still hold. What changed is the shape of the problem.

Python users already have application transactions managed by raw drivers or ORM/session layers. Forcing them onto AwaTransaction creates adoption friction for the common case where they already have:

  • an asyncpg transaction in a FastAPI service
  • a psycopg connection in a sync app
  • a SQLAlchemy Session / AsyncSession
  • a Django transaction.atomic() block

The question is whether Awa should support transactional enqueue within those existing transactions, and if so, how far that support extends.

Decision

Expose a public Python bridge module, awa.bridge, with insert-only helpers that execute on the caller's existing transactional connection or session:

  • insert_job(conn_or_session, args, ...)
  • insert_job_sync(conn_or_session, args, ...)

Supported surfaces

  • asyncpg.Connection
  • psycopg.AsyncConnection
  • psycopg.Connection
  • SQLAlchemy AsyncConnection
  • SQLAlchemy AsyncSession
  • SQLAlchemy Connection
  • SQLAlchemy Session
  • Django database connection inside transaction.atomic()

Design constraints

  • Insert-only. The bridge inserts jobs; it does not expose a general query API.
  • No connection sharing. Awa does not share protocol state with another driver. The bridge executes SQL through the caller's own driver/session object.
  • Participates in the caller's transaction. Commit/rollback semantics are owned by the application framework or driver already in use.
  • Runtime detection is acceptable in Python. The bridge may inspect the passed object and dispatch to the appropriate adapter path.
  • Parity with core insert semantics is required. Validation, state determination, and insert defaults must match the main Awa insert path, with dedicated tests guarding that parity.

Scope boundaries

  • No worker/runtime bridging. Polling, dispatch, heartbeating, retries, and admin operations remain on Awa's own runtime/client surfaces.
  • No transaction lifecycle management. The bridge does not begin, commit, roll back, or manage savepoints for external transactions.
  • No ORM abstraction layer. We support specific widely used connection/session types, not arbitrary ORM adapters.
  • No batch/COPY bridge. The bridge is for app-owned transactional enqueue of one or a few jobs. Awa's high-throughput bulk path is a driver-specific staging-and-COPY implementation, not a thin adapter surface, so bulk ingestion remains on Awa's native sqlx-backed APIs until there is a clear need for per-driver, explicitly tested bulk adapters.
  • No promise of every wrapper working. Wrapper compatibility is explicit and test-backed, not implicit.

Consequences

  • Python teams can adopt Awa incrementally without rewriting application data access around AwaTransaction.
  • The supported bridge surfaces become a compatibility commitment and must stay exercised in CI.
  • ADR-006 remains valid for the core principle that Awa should not share database connections across drivers or become an ORM integration layer.
  • The bridge has a broader support matrix than AwaTransaction, so semantic drift is the main maintenance risk; parity tests are the primary mitigation.

Relationship to ADR-016

ADR-016 applies the same insert-only bridge principle to Rust driver integrations. Rust adapters use the public awa::adapter::postgres preparation and SQL contract; Python bridges keep their Python-facing API but must preserve the same validation, state selection, uniqueness, and transaction-participation semantics.