db.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. """过程库 CkStore:写 / 读 creation_knowledge.ck_post、ck_knowledge_item。
  2. 目标实例是 Greenplum(PG12 内核),注意:
  3. - 不用 INSERT ... ON CONFLICT(Greenplum 分布表支持不稳):upsert = 先 UPDATE,rowcount=0 再 INSERT。
  4. - 不更新分布键 id。
  5. - JSONB 列用 psycopg2.extras.Json 包装。
  6. 连接 / JSONB / 参数化风格对齐 ContentFindAgentNew 的 database_runtime.py。
  7. """
  8. from __future__ import annotations
  9. from typing import Any, Callable, Optional
  10. import psycopg2
  11. import psycopg2.extras
  12. from creation_knowledge.config import PgConfig
  13. from creation_knowledge.models import Post
  14. ConnectionFactory = Callable[[], Any]
  15. def _connect(cfg: PgConfig) -> Any:
  16. return psycopg2.connect(
  17. host=cfg.host,
  18. port=cfg.port,
  19. user=cfg.user,
  20. password=cfg.password,
  21. dbname=cfg.database,
  22. connect_timeout=cfg.timeout,
  23. options=f"-c search_path={cfg.schema}",
  24. )
  25. class CkStore:
  26. def __init__(
  27. self,
  28. config: PgConfig,
  29. connection_factory: ConnectionFactory | None = None,
  30. ) -> None:
  31. self.config = config
  32. self._connection_factory = connection_factory or (lambda: _connect(config))
  33. # ---------- 写 ----------
  34. def upsert_post(self, post: Post) -> None:
  35. """落 fetch 产物:raw / cards 等基础字段,stage=fetched。先 UPDATE 后 INSERT。"""
  36. cards = psycopg2.extras.Json([c.model_dump() for c in post.cards])
  37. with self._connection_factory() as conn:
  38. with conn.cursor() as cur:
  39. cur.execute(
  40. "UPDATE ck_post SET platform=%s, url=%s, raw=%s, cards=%s, "
  41. "updated_at=now() WHERE id=%s",
  42. (post.platform, post.url, psycopg2.extras.Json(post.raw),
  43. cards, post.id),
  44. )
  45. if cur.rowcount == 0:
  46. cur.execute(
  47. "INSERT INTO ck_post (id, platform, url, raw, cards, stage) "
  48. "VALUES (%s, %s, %s, %s, %s, 'fetched')",
  49. (post.id, post.platform, post.url,
  50. psycopg2.extras.Json(post.raw), cards),
  51. )
  52. conn.commit()
  53. def set_extracted(self, post_id: str, extracted: dict, stage: str = "extracted") -> None:
  54. self._set_json(post_id, "extracted", extracted, stage)
  55. def set_screening(self, post_id: str, screening: dict, stage: str = "screened") -> None:
  56. self._set_json(post_id, "screening", screening, stage)
  57. def _set_json(self, post_id: str, column: str, value: dict, stage: str) -> None:
  58. with self._connection_factory() as conn:
  59. with conn.cursor() as cur:
  60. cur.execute(
  61. f"UPDATE ck_post SET {column}=%s, stage=%s, updated_at=now() "
  62. "WHERE id=%s",
  63. (psycopg2.extras.Json(value), stage, post_id),
  64. )
  65. conn.commit()
  66. def update_stage(self, post_id: str, stage: str) -> None:
  67. with self._connection_factory() as conn:
  68. with conn.cursor() as cur:
  69. cur.execute(
  70. "UPDATE ck_post SET stage=%s, updated_at=now() WHERE id=%s",
  71. (stage, post_id),
  72. )
  73. conn.commit()
  74. def save_item(
  75. self,
  76. post_id: str,
  77. item: dict,
  78. deconstruction: Optional[dict] = None,
  79. ingest_payload: Optional[dict] = None,
  80. ) -> int:
  81. """插入一条知识片段,返回自增 id。ingest_status 默认 pending。"""
  82. with self._connection_factory() as conn:
  83. with conn.cursor() as cur:
  84. cur.execute(
  85. "INSERT INTO ck_knowledge_item "
  86. "(post_id, item, deconstruction, ingest_payload, ingest_status) "
  87. "VALUES (%s, %s, %s, %s, 'pending') RETURNING id",
  88. (
  89. post_id,
  90. psycopg2.extras.Json(item),
  91. psycopg2.extras.Json(deconstruction) if deconstruction else None,
  92. psycopg2.extras.Json(ingest_payload) if ingest_payload else None,
  93. ),
  94. )
  95. item_id = cur.fetchone()[0]
  96. conn.commit()
  97. return int(item_id)
  98. def update_item_ingest(
  99. self, item_id: int, status: str, knowledge_id: Optional[str] = None
  100. ) -> None:
  101. with self._connection_factory() as conn:
  102. with conn.cursor() as cur:
  103. cur.execute(
  104. "UPDATE ck_knowledge_item SET ingest_status=%s, knowledge_id=%s, "
  105. "updated_at=now() WHERE id=%s",
  106. (status, knowledge_id, item_id),
  107. )
  108. conn.commit()
  109. def delete_post(self, post_id: str) -> None:
  110. """删除帖子及其片段(用于校验探测的清理)。"""
  111. with self._connection_factory() as conn:
  112. with conn.cursor() as cur:
  113. cur.execute("DELETE FROM ck_knowledge_item WHERE post_id=%s", (post_id,))
  114. cur.execute("DELETE FROM ck_post WHERE id=%s", (post_id,))
  115. conn.commit()
  116. # ---------- 读 ----------
  117. def read_post(self, post_id: str) -> Optional[dict]:
  118. return self._fetch_one(
  119. "SELECT * FROM ck_post WHERE id=%s", (post_id,)
  120. )
  121. def read_items(self, post_id: str) -> list[dict]:
  122. return self._fetch_all(
  123. "SELECT * FROM ck_knowledge_item WHERE post_id=%s ORDER BY id", (post_id,)
  124. )
  125. def list_posts(self, limit: int = 100) -> list[dict]:
  126. return self._fetch_all(
  127. "SELECT id, platform, url, stage, created_at, updated_at "
  128. "FROM ck_post ORDER BY created_at DESC LIMIT %s",
  129. (limit,),
  130. )
  131. def posts_overview(self, limit: int = 100) -> list[dict]:
  132. """列表页用:每帖基础字段 + 知识片段数。"""
  133. return self._fetch_all(
  134. "SELECT p.id, p.platform, p.url, p.stage, p.created_at, p.updated_at, "
  135. "count(k.id) AS item_count "
  136. "FROM ck_post p LEFT JOIN ck_knowledge_item k ON k.post_id = p.id "
  137. "GROUP BY p.id, p.platform, p.url, p.stage, p.created_at, p.updated_at "
  138. "ORDER BY p.created_at DESC LIMIT %s",
  139. (limit,),
  140. )
  141. def table_columns(self, table: str) -> list[str]:
  142. rows = self._fetch_all(
  143. "SELECT column_name FROM information_schema.columns "
  144. "WHERE table_schema=%s AND table_name=%s ORDER BY ordinal_position",
  145. (self.config.schema, table),
  146. )
  147. return [r["column_name"] for r in rows]
  148. def _fetch_one(self, sql: str, params: tuple) -> Optional[dict]:
  149. with self._connection_factory() as conn:
  150. with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
  151. cur.execute(sql, params)
  152. row = cur.fetchone()
  153. return dict(row) if row else None
  154. def _fetch_all(self, sql: str, params: tuple) -> list[dict]:
  155. with self._connection_factory() as conn:
  156. with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
  157. cur.execute(sql, params)
  158. return [dict(r) for r in cur.fetchall()]