Explorar el Código

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.
maziggy hace 2 meses
padre
commit
8faaeb96e0
Se han modificado 2 ficheros con 27 adiciones y 3 borrados
  1. 24 0
      backend/app/core/database.py
  2. 3 3
      backend/app/schemas/archive.py

+ 24 - 0
backend/app/core/database.py

@@ -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."""

+ 3 - 3
backend/app/schemas/archive.py

@@ -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