db.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 等基础字段,stage=fetched。先 UPDATE 后 INSERT。"""
  36. with self._connection_factory() as conn:
  37. with conn.cursor() as cur:
  38. cur.execute(
  39. "UPDATE ck_post SET platform=%s, url=%s, raw=%s, "
  40. "updated_at=now() WHERE id=%s",
  41. (post.platform, post.url, psycopg2.extras.Json(post.raw), post.id),
  42. )
  43. if cur.rowcount == 0:
  44. cur.execute(
  45. "INSERT INTO ck_post (id, platform, url, raw, stage) "
  46. "VALUES (%s, %s, %s, %s, 'fetched')",
  47. (post.id, post.platform, post.url,
  48. psycopg2.extras.Json(post.raw)),
  49. )
  50. conn.commit()
  51. def set_extracted(self, post_id: str, extracted: dict, stage: str = "extracted") -> None:
  52. self._set_json(post_id, "extracted", extracted, stage)
  53. def set_screening(self, post_id: str, screening: dict, stage: str = "screened") -> None:
  54. self._set_json(post_id, "screening", screening, stage)
  55. def _set_json(self, post_id: str, column: str, value: dict, stage: str) -> None:
  56. with self._connection_factory() as conn:
  57. with conn.cursor() as cur:
  58. cur.execute(
  59. f"UPDATE ck_post SET {column}=%s, stage=%s, updated_at=now() "
  60. "WHERE id=%s",
  61. (psycopg2.extras.Json(value), stage, post_id),
  62. )
  63. conn.commit()
  64. def update_stage(self, post_id: str, stage: str) -> None:
  65. with self._connection_factory() as conn:
  66. with conn.cursor() as cur:
  67. cur.execute(
  68. "UPDATE ck_post SET stage=%s, updated_at=now() WHERE id=%s",
  69. (stage, post_id),
  70. )
  71. conn.commit()
  72. def save_item(
  73. self,
  74. post_id: str,
  75. item: dict,
  76. deconstruction: Optional[dict] = None,
  77. ingest_payload: Optional[dict] = None,
  78. ) -> int:
  79. """插入一条知识片段,返回自增 id。ingest_status 默认 pending。"""
  80. with self._connection_factory() as conn:
  81. with conn.cursor() as cur:
  82. cur.execute(
  83. "INSERT INTO ck_knowledge_item "
  84. "(post_id, item, deconstruction, ingest_payload, ingest_status) "
  85. "VALUES (%s, %s, %s, %s, 'pending') RETURNING id",
  86. (
  87. post_id,
  88. psycopg2.extras.Json(item),
  89. psycopg2.extras.Json(deconstruction) if deconstruction else None,
  90. psycopg2.extras.Json(ingest_payload) if ingest_payload else None,
  91. ),
  92. )
  93. item_id = cur.fetchone()[0]
  94. conn.commit()
  95. return int(item_id)
  96. def update_item_ingest(
  97. self, item_id: int, status: str, knowledge_id: Optional[str] = None
  98. ) -> None:
  99. with self._connection_factory() as conn:
  100. with conn.cursor() as cur:
  101. cur.execute(
  102. "UPDATE ck_knowledge_item SET ingest_status=%s, knowledge_id=%s, "
  103. "updated_at=now() WHERE id=%s",
  104. (status, knowledge_id, item_id),
  105. )
  106. conn.commit()
  107. def delete_post(self, post_id: str) -> None:
  108. """删除帖子及其片段(用于校验探测的清理)。"""
  109. with self._connection_factory() as conn:
  110. with conn.cursor() as cur:
  111. cur.execute("DELETE FROM ck_knowledge_item WHERE post_id=%s", (post_id,))
  112. cur.execute("DELETE FROM ck_post WHERE id=%s", (post_id,))
  113. conn.commit()
  114. # ---------- 读 ----------
  115. def read_post(self, post_id: str) -> Optional[dict]:
  116. return self._fetch_one(
  117. "SELECT * FROM ck_post WHERE id=%s", (post_id,)
  118. )
  119. def read_items(self, post_id: str) -> list[dict]:
  120. return self._fetch_all(
  121. "SELECT * FROM ck_knowledge_item WHERE post_id=%s ORDER BY id", (post_id,)
  122. )
  123. def list_posts(self, limit: int = 100) -> list[dict]:
  124. return self._fetch_all(
  125. "SELECT id, platform, url, stage, created_at, updated_at "
  126. "FROM ck_post ORDER BY created_at DESC LIMIT %s",
  127. (limit,),
  128. )
  129. def table_columns(self, table: str) -> list[str]:
  130. rows = self._fetch_all(
  131. "SELECT column_name FROM information_schema.columns "
  132. "WHERE table_schema=%s AND table_name=%s ORDER BY ordinal_position",
  133. (self.config.schema, table),
  134. )
  135. return [r["column_name"] for r in rows]
  136. def _fetch_one(self, sql: str, params: tuple) -> Optional[dict]:
  137. with self._connection_factory() as conn:
  138. with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
  139. cur.execute(sql, params)
  140. row = cur.fetchone()
  141. return dict(row) if row else None
  142. def _fetch_all(self, sql: str, params: tuple) -> list[dict]:
  143. with self._connection_factory() as conn:
  144. with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
  145. cur.execute(sql, params)
  146. return [dict(r) for r in cur.fetchall()]