86 lines
3.5 KiB
Python
86 lines
3.5 KiB
Python
"""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)
|
|
VALUES (%s, %s, %s, %s, %s, %s)
|
|
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),
|
|
)
|