Fix migration: parse ISO timestamp strings to datetime objects for asyncpg
asyncpg requires datetime instances for TIMESTAMPTZ columns, not strings. Added _parse_dt() helper that converts ISO strings (with or without tz) to timezone-aware datetime, falling back to NOW() if the value is missing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1a1bc97bfc
commit
d8d1dfeff5
1 changed files with 26 additions and 10 deletions
|
|
@ -13,11 +13,25 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_dt(value) -> datetime | None:
|
||||
"""Parse an ISO timestamp string (or None) into a timezone-aware datetime."""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
dt = datetime.fromisoformat(value)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
async def migrate():
|
||||
# Import here to avoid circular issues at module level
|
||||
from ..config_runtime import server_config
|
||||
|
|
@ -38,16 +52,16 @@ async def migrate():
|
|||
with open(users_file) as f:
|
||||
users = json.load(f)
|
||||
count = 0
|
||||
now = datetime.now(timezone.utc)
|
||||
for uid, u in users.items():
|
||||
await conn.execute('''
|
||||
INSERT INTO users (id, email, name, role, active, created_at, last_seen_at)
|
||||
VALUES ($1, $2, $3, $4, $5,
|
||||
COALESCE($6::timestamptz, NOW()),
|
||||
COALESCE($7::timestamptz, NOW()))
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
''', uid, u.get('email', ''), u.get('name', ''),
|
||||
u.get('role', 'user'), u.get('active', True),
|
||||
u.get('created'), u.get('last_seen'))
|
||||
_parse_dt(u.get('created')) or now,
|
||||
_parse_dt(u.get('last_seen')) or now)
|
||||
count += 1
|
||||
logger.info(f"Migrated {count} users")
|
||||
|
||||
|
|
@ -56,12 +70,14 @@ async def migrate():
|
|||
if os.path.exists(clients_file):
|
||||
with open(clients_file) as f:
|
||||
clients = json.load(f)
|
||||
now = datetime.now(timezone.utc)
|
||||
for c in clients:
|
||||
await conn.execute('''
|
||||
INSERT INTO clients (id, name, has_custom_dropdowns, created_at)
|
||||
VALUES ($1, $2, $3, COALESCE($4::timestamptz, NOW()))
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
''', c['id'], c['name'], c.get('hasCustomDropdowns', False), c.get('created'))
|
||||
''', c['id'], c['name'], c.get('hasCustomDropdowns', False),
|
||||
_parse_dt(c.get('created')) or now)
|
||||
logger.info(f"Migrated {len(clients)} clients")
|
||||
|
||||
# ── Global dropdowns ───────────────────────────────────────────────────
|
||||
|
|
@ -157,16 +173,16 @@ async def migrate():
|
|||
data = json.load(f)
|
||||
|
||||
client_id = sheet_meta.get('client_id') or None
|
||||
now = datetime.now(timezone.utc)
|
||||
await conn.execute('''
|
||||
INSERT INTO sheets
|
||||
(id, user_id, name, client_id, data, item_count, created_at, modified_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6,
|
||||
COALESCE($7::timestamptz, NOW()),
|
||||
COALESCE($8::timestamptz, NOW()))
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
''', sid, user_id, sheet_meta.get('name', 'Untitled'),
|
||||
client_id, data, len(data),
|
||||
sheet_meta.get('created'), sheet_meta.get('modified'))
|
||||
_parse_dt(sheet_meta.get('created')) or now,
|
||||
_parse_dt(sheet_meta.get('modified')) or now)
|
||||
total += 1
|
||||
logger.info(f"Migrated {total} sheets")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue