Skip to content

Database error visibility — follow-up gaps remaining after #46 fix #49

@MichaelC8E

Description

@MichaelC8E

Following on from #46, which addressed the main visibility concern for queries outside explicit transactions — original PG cause now reaches both the exception and Log.error for the documented call shape (verified on 3.13.8 against PG 18). The cascade "current transaction is aborted, commands ignored until end of transaction block" no longer surfaces when calling Model.where(...) or Database.execute(...) outside start_transaction().

Three paths in the same defect class still reproduce on 3.13.8.

Gap 1 — fetch() inside db.start_transaction()

db.start_transaction()
GiftCard.where("created_by_email = ? AND is_deleted = 0", [email])

Surfaces InFailedSqlTransaction: current transaction is aborted, commands ignored until end of transaction block. The original UndefinedFunction: operator does not exist: boolean = integer is lost.

_heal_aborted_txn() (postgres.py:79) and _on_query_error()'s auto-rollback (postgres.py:162) both defer when self._in_transaction is True. Stated reasoning at postgres.py:127-130"Inside an explicit transaction we do NOT auto-rollback — the user owns that transaction." — preserves the SAVEPOINT/retry contract, but the visibility half goes with it.

Gap 2 — execute() inside db.start_transaction() (silent failure)

db.start_transaction()
db.execute("UPDATE gift_cards SET is_deleted = 0 WHERE id = 1")   # returns False, last_error = original cause
db.execute("UPDATE gift_cards SET created_by_email = 'x' WHERE id = 1")  # returns False, last_error = cascade

Database.execute() (connection.py:412-414) catches every exception and returns False. Inside a transaction, the second call on a poisoned connection also returns False — caller has no exception to catch and no way to distinguish a real failure from a cascade without parsing db.last_error, which by the second call has been overwritten by the cascade message.

Gap 3 — db.last_error asymmetric between fetch() and execute()

Surface After fetch() failure After execute() failure
db.get_error() None original cause
adapter.last_error original cause original cause

Database.fetch() (connection.py:438-439) routes straight to adapter.fetch() with no last_error capture. Database.execute() (connection.py:412-414) does capture. The adapter's _on_query_error (postgres.py:154) writes self.last_error = str(exc) either way — the Database wrapper just doesn't read it on the fetch() path.

Potential fix shapes

  • Gaps 1 + 2Log.warning from _on_query_error even when inside an explicit transaction, so the original cause is at least in the log when auto-rollback is skipped. No behavioural change for callers running SAVEPOINT/retry patterns; closes the visibility half.
  • Gap 3 — one-line capture in Database.fetch() mirroring Database.execute(). fetch() still re-raises; only db.get_error() populates.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions