mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(archives): backfill NULL created_at + tolerate NULL in response (#1732)
Older print_archives rows (and rows that landed via the SQLite ↔ Postgres
cross-DB restore path) can have created_at = NULL because the column was
originally created without a DEFAULT clause — server_default=func.now()
only fires at table creation, not for existing rows or raw cross-DB
inserts. The list_archives response model required a datetime, so a
single NULL row 500'd the whole endpoint via Pydantic ResponseValidationError.
- Boot-time backfill: COALESCE(completed_at, started_at, now()) for
any row where created_at IS NULL. Dialect-branched (SQLite datetime('now')
vs Postgres NOW()).
- Schema: created_at is now Optional on ArchiveDuplicate, ArchiveResponse,
and ArchiveSlim so a future NULL-leaking path doesn't break the list
endpoint again.
This commit is contained in:
@@ -2914,6 +2914,30 @@ async def run_migrations(conn):
|
||||
# file metadata so the FileManager displays the filename, not the title (#1489).
|
||||
await _migrate_drop_library_print_name(conn)
|
||||
|
||||
# Backfill NULL print_archives.created_at — older rows (and rows imported
|
||||
# via the SQLite ↔ Postgres cross-DB restore path) can land with NULL
|
||||
# because the column was originally created without a DEFAULT clause and
|
||||
# server_default=func.now() only fires at table creation, not column
|
||||
# population. The list_archives response model requires a datetime, so a
|
||||
# single NULL row 500s the whole endpoint (#1732).
|
||||
async with conn.begin_nested():
|
||||
if is_sqlite():
|
||||
await conn.execute(
|
||||
text(
|
||||
"UPDATE print_archives "
|
||||
"SET created_at = COALESCE(completed_at, started_at, datetime('now')) "
|
||||
"WHERE created_at IS NULL"
|
||||
)
|
||||
)
|
||||
else:
|
||||
await conn.execute(
|
||||
text(
|
||||
"UPDATE print_archives "
|
||||
"SET created_at = COALESCE(completed_at, started_at, NOW()) "
|
||||
"WHERE created_at IS NULL"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def seed_notification_templates():
|
||||
"""Seed default notification templates if they don't exist."""
|
||||
|
||||
@@ -27,7 +27,7 @@ class ArchiveDuplicate(BaseModel):
|
||||
|
||||
id: int
|
||||
print_name: str | None
|
||||
created_at: datetime
|
||||
created_at: datetime | None
|
||||
match_type: str # "exact" (hash match) or "similar" (name match)
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ class ArchiveResponse(BaseModel):
|
||||
energy_kwh: float | None = None
|
||||
energy_cost: float | None = None
|
||||
|
||||
created_at: datetime
|
||||
created_at: datetime | None
|
||||
|
||||
# User tracking (Issue #206)
|
||||
created_by_id: int | None = None
|
||||
@@ -137,7 +137,7 @@ class ArchiveSlim(BaseModel):
|
||||
completed_at: datetime | None
|
||||
cost: float | None
|
||||
quantity: int = 1
|
||||
created_at: datetime
|
||||
created_at: datetime | None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
Reference in New Issue
Block a user