vignette/apps/api/app/db.py
Yun Chan 84eb6e2173 feat(engine): claude -p 상주 멀티턴 엔진 게이트웨이 + 실동작 검증
- engine_gateway/gateway.py: 회기당 claude -p 상주 프로세스(stream-json), 턴 직렬, budget 제한
- 세션 생성/턴/종료 HTTP API(FastAPI, :9099)
- 검증: 멀티턴 컨텍스트 유지 + prompt caching 재사용(턴2 +$0.07) 실동작 확인
2026-06-25 21:43:27 +09:00

126 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""asyncpg 연결 풀 + pgvector 등록.
DB = NAS PostgreSQL 16 단일 SoR (마스터플랜 §0). 스키마 4분할: app / kb / audit / ds.
RLS 이중강제: 커넥션 획득 시 SET LOCAL app.current_role / app.current_ai_view 주입
(deps.py 의 RBAC 의존성과 짝). 여기선 풀 + 헬퍼만 제공한다.
"""
from __future__ import annotations
import json
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Optional
import asyncpg
from .config import settings
# 전역 풀 핸들. main.py lifespan 에서 init/close.
_pool: Optional[asyncpg.Pool] = None
async def _init_connection(conn: asyncpg.Connection) -> None:
"""커넥션 단위 코덱 등록.
- jsonb: dict 직렬화 자동 (asyncpg 기본은 str 반환)
- vector(1024): pgvector. 런타임 인코딩은 RAG 경로에서 처리(여기선 텍스트 캐스트 허용).
TODO: pgvector 바이너리 코덱 등록(register_vector) — RAG 라우터 구현 시 BGE-M3 1024d 연동.
"""
await conn.set_type_codec(
"jsonb",
encoder=lambda v: json.dumps(v, ensure_ascii=False),
decoder=json.loads,
schema="pg_catalog",
)
await conn.set_type_codec(
"json",
encoder=lambda v: json.dumps(v, ensure_ascii=False),
decoder=json.loads,
schema="pg_catalog",
)
async def init_pool() -> asyncpg.Pool:
"""풀 생성 (main lifespan startup)."""
global _pool
if _pool is not None:
return _pool
_pool = await asyncpg.create_pool(
dsn=settings.database_url,
min_size=settings.db_pool_min_size,
max_size=settings.db_pool_max_size,
command_timeout=settings.db_command_timeout,
init=_init_connection,
)
return _pool
async def close_pool() -> None:
"""풀 종료 (main lifespan shutdown)."""
global _pool
if _pool is not None:
await _pool.close()
_pool = None
def get_pool() -> asyncpg.Pool:
"""초기화된 풀 반환. lifespan 밖 호출 시 RuntimeError."""
if _pool is None:
raise RuntimeError("DB pool not initialized — init_pool() must run in lifespan startup")
return _pool
@asynccontextmanager
async def acquire(
*,
role: Optional[str] = None,
ai_view: Optional[str] = None,
) -> AsyncIterator[asyncpg.Connection]:
"""커넥션 획득 + RLS 컨텍스트 주입.
RLS 이중강제 (설계서 §4.1, F-30):
레이어1 = AI 정보비대칭: app.current_ai_view (visible_to[] WHERE 강제)
레이어2 = 인간 RBAC×cohort: app.current_role (RLS 정책)
트랜잭션 내 SET LOCAL 로 주입해 커넥션 풀 재사용 시 누수 방지.
NOTE: RLS 정책/세션변수는 Phase 0 마이그레이션에서 정의(설계서 §3.3 / §4).
여기선 변수 주입 계약만 확정.
"""
pool = get_pool()
async with pool.acquire() as conn:
async with conn.transaction():
if role is not None:
await conn.execute("SELECT set_config('app.current_role', $1, true)", role)
if ai_view is not None:
await conn.execute("SELECT set_config('app.current_ai_view', $1, true)", ai_view)
yield conn
async def healthcheck() -> bool:
"""SELECT 1 핑. /health 에서 사용."""
try:
pool = get_pool()
async with pool.acquire() as conn:
val = await conn.fetchval("SELECT 1")
return val == 1
except Exception:
return False
async def fetch(query: str, *args: Any, role: Optional[str] = None, ai_view: Optional[str] = None):
async with acquire(role=role, ai_view=ai_view) as conn:
return await conn.fetch(query, *args)
async def fetchrow(
query: str, *args: Any, role: Optional[str] = None, ai_view: Optional[str] = None
):
async with acquire(role=role, ai_view=ai_view) as conn:
return await conn.fetchrow(query, *args)
async def execute(
query: str, *args: Any, role: Optional[str] = None, ai_view: Optional[str] = None
) -> str:
async with acquire(role=role, ai_view=ai_view) as conn:
return await conn.execute(query, *args)