db_session.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Formal PostgreSQL session helpers for the creation-knowledge state store."""
  2. from __future__ import annotations
  3. from contextlib import contextmanager
  4. from threading import Lock
  5. from typing import Any, Iterator
  6. import psycopg2
  7. import psycopg2.extras
  8. from psycopg2.pool import ThreadedConnectionPool
  9. from core.config import CreationDbConfig
  10. psycopg2.extras.register_uuid()
  11. _pool: ThreadedConnectionPool | None = None
  12. _pool_key: tuple[Any, ...] | None = None
  13. _pool_lock = Lock()
  14. def connect(config: CreationDbConfig) -> Any:
  15. """Open a PostgreSQL connection scoped to the formal state schema."""
  16. return psycopg2.connect(
  17. host=config.host,
  18. port=config.port,
  19. user=config.user,
  20. password=config.password,
  21. dbname=config.database,
  22. connect_timeout=config.timeout,
  23. application_name=config.application_name,
  24. options=f"-c search_path={config.schema},public",
  25. )
  26. def _connection_kwargs(config: CreationDbConfig) -> dict[str, Any]:
  27. return {
  28. "host": config.host,
  29. "port": config.port,
  30. "user": config.user,
  31. "password": config.password,
  32. "dbname": config.database,
  33. "connect_timeout": config.timeout,
  34. "application_name": config.application_name,
  35. "options": f"-c search_path={config.schema},public",
  36. }
  37. def get_pool(config: CreationDbConfig) -> ThreadedConnectionPool:
  38. """Return a process-local pool for the current creation DB config."""
  39. global _pool, _pool_key
  40. key = (
  41. config.host,
  42. config.port,
  43. config.user,
  44. config.database,
  45. config.schema,
  46. config.application_name,
  47. config.pool_min,
  48. config.pool_max,
  49. )
  50. with _pool_lock:
  51. if _pool is not None and _pool_key == key:
  52. return _pool
  53. if _pool is not None:
  54. _pool.closeall()
  55. _pool = ThreadedConnectionPool(
  56. minconn=max(1, config.pool_min),
  57. maxconn=max(config.pool_min, config.pool_max),
  58. **_connection_kwargs(config),
  59. )
  60. _pool_key = key
  61. return _pool
  62. @contextmanager
  63. def transaction(config: CreationDbConfig) -> Iterator[Any]:
  64. """Yield a connection and commit or roll back around the caller's work."""
  65. conn = connect(config)
  66. try:
  67. yield conn
  68. conn.commit()
  69. except Exception:
  70. conn.rollback()
  71. raise
  72. finally:
  73. conn.close()
  74. @contextmanager
  75. def pooled_transaction(config: CreationDbConfig) -> Iterator[Any]:
  76. """Yield a pooled connection and return it to the pool after the request."""
  77. pool = get_pool(config)
  78. conn = pool.getconn()
  79. try:
  80. yield conn
  81. conn.commit()
  82. except Exception:
  83. conn.rollback()
  84. raise
  85. finally:
  86. pool.putconn(conn, close=bool(getattr(conn, "closed", False)))
  87. def fetch_all(
  88. config: CreationDbConfig,
  89. sql: str,
  90. params: tuple[Any, ...] = (),
  91. ) -> list[dict[str, Any]]:
  92. """Small read helper for diagnostics and repository smoke checks."""
  93. conn = connect(config)
  94. try:
  95. with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
  96. cur.execute(sql, params)
  97. return [dict(row) for row in cur.fetchall()]
  98. finally:
  99. conn.close()