"""PostgreSQL — источник истины (psycopg).""" from __future__ import annotations import hashlib import json import psycopg from psycopg.rows import dict_row from . import config def connect(): return psycopg.connect(config.PG_DSN, row_factory=dict_row) def _as_text(content) -> str: """Привести content к строке: dict/list -> JSON, иначе str.""" if isinstance(content, str): return content import json as _json return _json.dumps(content, ensure_ascii=False, sort_keys=True) def content_hash(content) -> str: return hashlib.sha256(_as_text(content).encode("utf-8")).hexdigest() def upsert_document(conn, *, kind: str, uri: str | None, title: str | None, content_hash: str | None = None, authority: float = 0.5, meta: dict | None = None) -> int: """Вставить/вернуть документ по uri.""" with conn.cursor() as cur: if uri: cur.execute("SELECT id FROM pm_document WHERE uri = %s", (uri,)) row = cur.fetchone() if row: return row["id"] cur.execute( """INSERT INTO pm_document (kind, uri, title, content_hash, authority, meta, fetched_at) VALUES (%s, %s, %s, %s, %s, %s, now()) ON CONFLICT (uri) DO UPDATE SET title = EXCLUDED.title RETURNING id""", (kind, uri, title, content_hash, authority, json.dumps(meta or {}, ensure_ascii=False)), ) return cur.fetchone()["id"] def upsert_atom(conn, *, type: str, claim: str | None, content, confidence: float = 0.5, status: str = "candidate", doc_id: int | None = None, locator: dict | None = None, metadata: dict | None = None) -> tuple[int, bool]: """Вставить атом по content_hash. Возвращает (id, created). content может быть str (text/fact/definition/code) или dict/list (logical_rule/table) — dict сериализуется в JSON. """ content = _as_text(content) h = content_hash(content) with conn.cursor() as cur: cur.execute("SELECT id FROM pm_atom WHERE content_hash = %s", (h,)) row = cur.fetchone() if row: return row["id"], False cur.execute( """INSERT INTO pm_atom (type, claim, content, content_hash, confidence, status, doc_id, locator, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id""", (type, claim, content, h, confidence, status, doc_id, json.dumps(locator or {}, ensure_ascii=False), json.dumps(metadata or {}, ensure_ascii=False)), ) return cur.fetchone()["id"], True def upsert_link(conn, *, src: int, dst: int, kind: str, confidence: float = 0.5) -> int: with conn.cursor() as cur: cur.execute( """INSERT INTO pm_atom_link (src, dst, kind, confidence) VALUES (%s, %s, %s, %s) ON CONFLICT (src, dst, kind) DO NOTHING RETURNING id""", (src, dst, kind, confidence), ) row = cur.fetchone() return row["id"] if row else 0 def enqueue_outbox(conn, *, entity_type: str, entity_id: int, op: str = "upsert"): with conn.cursor() as cur: cur.execute( "INSERT INTO pm_outbox (entity_type, entity_id, op) VALUES (%s, %s, %s)", (entity_type, entity_id, op), )