""" Phase 1 — Step 1.11: heartbeats (SQLite → Postgres) Raw-SQL table (no ORM model). BIGSERIAL PK — SQLite IDs not preserved. Run on VPS: docker compose exec backend python -m migration.migrate_heartbeats """ import asyncio import sys from sqlalchemy import text from migration.utils import open_sqlite, AsyncPgSession, parse_dt, log_run, pg_count SCRIPT = "migrate_heartbeats" async def run() -> None: sqlite = await open_sqlite() rows = await sqlite.execute_fetchall("SELECT * FROM heartbeats ORDER BY received_at") await sqlite.close() source_count = len(rows) print(f"Source (SQLite): {source_count} heartbeats rows") if source_count == 0: print("Nothing to migrate.") await log_run(SCRIPT, 0, 0, notes="source empty") return records = [ { "device_serial": r["device_serial"], "device_id": r["device_id"], "firmware_version": r["firmware_version"], "ip_address": r["ip_address"], "gateway": r["gateway"], "uptime_ms": r["uptime_ms"], "uptime_display": r["uptime_display"], "received_at": parse_dt(r["received_at"]), } for r in rows ] async with AsyncPgSession() as session: async with session.begin(): await session.execute( text(""" INSERT INTO heartbeats (device_serial, device_id, firmware_version, ip_address, gateway, uptime_ms, uptime_display, received_at) VALUES (:device_serial, :device_id, :firmware_version, :ip_address, :gateway, :uptime_ms, :uptime_display, :received_at) """), records, ) dest_count = await pg_count(session, "heartbeats") if dest_count < source_count: msg = f"Count mismatch: source={source_count} postgres={dest_count}" print(f"ERROR: {msg}", file=sys.stderr) await log_run(SCRIPT, source_count, dest_count, success=False, notes=msg) sys.exit(1) print(f"Postgres: {dest_count} rows ✓") await log_run(SCRIPT, source_count, dest_count) if __name__ == "__main__": asyncio.run(run())