from __future__ import annotations import sqlite3 from datetime import UTC, datetime from ..ddns_models import UpdateResult from .types import UpdateEventRow class RecordsEventsMixin: def upsert( self, hostname: str, ipv4: str | None, ipv6: str | None, update_ipv4: bool = True, update_ipv6: bool = True, ) -> UpdateResult: now = datetime.now(UTC).isoformat() with self.connect() as conn: existing = conn.execute("SELECT ipv4, ipv6 FROM records WHERE hostname = ?", (hostname,)).fetchone() current_ipv4 = existing["ipv4"] if existing else None current_ipv6 = existing["ipv6"] if existing else None effective_ipv4 = ipv4 if update_ipv4 else current_ipv4 effective_ipv6 = ipv6 if update_ipv6 else current_ipv6 changed = existing is None or current_ipv4 != effective_ipv4 or current_ipv6 != effective_ipv6 conn.execute( """ INSERT INTO records(hostname, ipv4, ipv6, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(hostname) DO UPDATE SET ipv4=excluded.ipv4, ipv6=excluded.ipv6, updated_at=excluded.updated_at """, (hostname, effective_ipv4, effective_ipv6, now), ) return UpdateResult( hostname=hostname, ipv4=effective_ipv4, ipv6=effective_ipv6, changed=changed, update_ipv4=update_ipv4, update_ipv6=update_ipv6, ) def commit_update_result(self, result: UpdateResult) -> UpdateResult: now = datetime.now(UTC).isoformat() with self.connect() as conn: conn.execute( """ INSERT INTO records(hostname, ipv4, ipv6, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(hostname) DO UPDATE SET ipv4=excluded.ipv4, ipv6=excluded.ipv6, updated_at=excluded.updated_at """, (result.hostname, result.ipv4, result.ipv6, now), ) return result def preview_update( self, hostname: str, ipv4: str | None, ipv6: str | None, update_ipv4: bool = True, update_ipv6: bool = True, ) -> UpdateResult: with self.connect() as conn: existing = conn.execute("SELECT ipv4, ipv6 FROM records WHERE hostname = ?", (hostname,)).fetchone() current_ipv4 = existing["ipv4"] if existing else None current_ipv6 = existing["ipv6"] if existing else None effective_ipv4 = ipv4 if update_ipv4 else current_ipv4 effective_ipv6 = ipv6 if update_ipv6 else current_ipv6 changed = existing is None or current_ipv4 != effective_ipv4 or current_ipv6 != effective_ipv6 return UpdateResult( hostname=hostname, ipv4=effective_ipv4, ipv6=effective_ipv6, changed=changed, update_ipv4=update_ipv4, update_ipv6=update_ipv6, ) def list_records(self) -> list[dict[str, str | None]]: return self.list_records_page(limit=10_000, offset=0) def list_records_page(self, limit: int = 100, offset: int = 0) -> list[dict[str, str | None]]: with self.connect() as conn: rows = conn.execute( """ SELECT hostname, ipv4, ipv6, updated_at FROM records ORDER BY hostname LIMIT ? OFFSET ? """, (limit, offset), ).fetchall() return [dict(row) for row in rows] def count_records(self) -> int: with self.connect() as conn: row = conn.execute("SELECT COUNT(*) FROM records").fetchone() return int(row[0]) def log_update_event( self, hostname: str, ipv4: str | None, ipv6: str | None, status: str, detail: str, source_ip: str, ) -> None: with self.connect() as conn: conn.execute( """ INSERT INTO update_events(hostname, ipv4, ipv6, status, detail, source_ip, created_at) VALUES (?, ?, ?, ?, ?, ?, ?) """, (hostname, ipv4, ipv6, status, detail[:500], source_ip, datetime.now(UTC).isoformat()), ) def log_update_rollup( self, hostname: str, ipv4: str | None, ipv6: str | None, status: str, detail: str, source_ip: str, ) -> None: now = datetime.now(UTC).isoformat() with self.connect() as conn: conn.execute( """ INSERT INTO update_event_rollups( hostname, ipv4, ipv6, status, detail, source_ip, first_seen_at, last_seen_at, count ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1) ON CONFLICT(hostname, ipv4, ipv6, status, detail, source_ip) DO UPDATE SET last_seen_at=excluded.last_seen_at, count=count + 1 """, (hostname, ipv4 or "", ipv6 or "", status, detail[:500], source_ip, now, now), ) def list_update_events(self, limit: int = 100, offset: int = 0) -> list[UpdateEventRow]: return self.list_update_events_page(limit=limit, offset=offset) def list_update_events_page(self, limit: int = 100, offset: int = 0) -> list[UpdateEventRow]: with self.connect() as conn: rows = conn.execute( """ SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, count FROM ( SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count FROM update_events UNION ALL SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count FROM update_event_rollups ) ORDER BY created_at DESC LIMIT ? OFFSET ? """, (limit, offset), ).fetchall() return [dict(row) for row in rows] def list_update_events_before(self, limit: int = 100, before: str | None = None) -> list[UpdateEventRow]: where_clause = "WHERE created_at < ?" if before else "" params: tuple[object, ...] = (before, limit) if before else (limit,) with self.connect() as conn: rows = conn.execute( f""" SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, count FROM ( SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count FROM update_events UNION ALL SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count FROM update_event_rollups ) {where_clause} ORDER BY created_at DESC LIMIT ? """, params, ).fetchall() return [dict(row) for row in rows] def count_update_events(self) -> int: with self.connect() as conn: event_count = conn.execute("SELECT COUNT(*) FROM update_events").fetchone()[0] rollup_count = conn.execute("SELECT COUNT(*) FROM update_event_rollups").fetchone()[0] return int(event_count) + int(rollup_count) def list_update_events_for_hostname(self, hostname: str, limit: int = 25) -> list[UpdateEventRow]: with self.connect() as conn: event_rows = conn.execute( """ SELECT hostname, ipv4, ipv6, status, detail, source_ip, created_at, 1 AS count FROM update_events WHERE hostname = ? ORDER BY created_at DESC LIMIT ? """, (hostname, limit), ).fetchall() rollup_rows = conn.execute( """ SELECT hostname, ipv4, ipv6, status, detail, source_ip, last_seen_at AS created_at, count FROM update_event_rollups WHERE hostname = ? ORDER BY last_seen_at DESC LIMIT ? """, (hostname, limit), ).fetchall() return _merge_event_rows(event_rows, rollup_rows, limit, 0) def _merge_event_rows( event_rows: list[sqlite3.Row], rollup_rows: list[sqlite3.Row], limit: int, offset: int, ) -> list[UpdateEventRow]: rows = [dict(row) for row in event_rows] rows.extend(dict(row) for row in rollup_rows) rows.sort(key=lambda row: str(row["created_at"]), reverse=True) return rows[offset : offset + limit]