NestQuestDatabase rejects a second transaction while one is active, so
two concurrent apply_migrations callers on one connection crashed with
RuntimeError instead of queueing. apply_migrations now holds a
per-(connection, loop) asyncio lock across the whole decide-and-apply
span; the queued runner re-reads the stamped version and applies
nothing. Deterministic concurrent-runner regression test added.
- apply_migrations re-reads the version under the transaction lock
before applying, so two interleaved runners on one connection cannot
both apply a non-idempotent migration from a stale read
- setup-failure test now spies on NestQuestDatabase.close and asserts
it ran and no runtime record was stored (was vacuous)
- __init__.py now calls apply_migrations() on setup instead of raw DDL
(HIGH finding: the runner was never invoked at startup); lifecycle
wiring tests in test_migrations.py cover fresh migrate, reload reuse,
and close-on-failure
- VERSION_TABLE DDL enforces singleton row (id=1 PRIMARY KEY + CHECK)
and version >= 1; stamp is INSERT .. ON CONFLICT DO UPDATE keyed on
the singleton row
- read_schema_version rejects non-integer and sub-1 versions with
explicit guards (bool excluded)
- no-op test asserts content hash + mtime, not just size