Files

57 lines
2.0 KiB
Python
Raw Permalink Normal View History

"""Atomic NCR number allocation.
Format: NCR-YYYY-NNNN (zero-padded, per-calendar-year sequence).
Strategy: UPDATE-first on the per-year row in ncr_sequences. The UPDATE takes
a row lock (InnoDB) / reserved write lock (SQLite) that is held until the
enclosing transaction commits, so two concurrent submissions serialize and can
never read the same sequence value. If the year row doesn't exist yet
(first NCR of a new year), it is inserted inside a SAVEPOINT; a losing racer
gets an IntegrityError, rolls back only the savepoint, and proceeds to the
UPDATE which now finds the winner's row.
"""
from datetime import datetime
from sqlalchemy import select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import NcrSequence
from app.models.base import utcnow
async def allocate_ncr_number(
db: AsyncSession, now: datetime | None = None
) -> tuple[str, int, int]:
"""Allocate the next NCR number inside the caller's transaction.
Returns (ncr_number, year, seq). Must be called within the same
transaction that inserts the NCR so the sequence row lock is held
until commit.
"""
year = (now or utcnow()).year
result = await db.execute(
update(NcrSequence)
.where(NcrSequence.year == year)
.values(last_seq=NcrSequence.last_seq + 1)
)
if result.rowcount == 0:
# First NCR of this calendar year — create the sequence row.
try:
async with db.begin_nested():
db.add(NcrSequence(year=year, last_seq=0))
await db.flush()
except IntegrityError:
pass # another request created it first; fall through to UPDATE
await db.execute(
update(NcrSequence)
.where(NcrSequence.year == year)
.values(last_seq=NcrSequence.last_seq + 1)
)
seq = (
await db.execute(select(NcrSequence.last_seq).where(NcrSequence.year == year))
).scalar_one()
return f"NCR-{year}-{seq:04d}", year, seq