Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
47413ea
PostgreSQL_연결_및_테이블_생성 : feat : env 환경변수 주입 https://github.com/Team-R…
Chuseok22 Dec 17, 2025
9066e5c
Merge branch 'main' into 20251217_#1_PostgreSQL_연결_및_테이블_생성
Chuseok22 Dec 22, 2025
45655d0
PostgreSQL_연결_및_테이블_생성 : feat : env 환경변수 주입을 위한 settings.py 추가 https:…
Chuseok22 Dec 22, 2025
280c968
PostgreSQL_연결_및_테이블_생성 : feat : postgresql DB 세션 연결 https://github.co…
Chuseok22 Dec 22, 2025
5beeb5c
PostgreSQL_연결_및_테이블_생성 : feat : source_type enum 추가 https://github.co…
Chuseok22 Dec 22, 2025
5cb7cef
PostgreSQL_연결_및_테이블_생성 : feat : GithubCursorEntity 추가 https://github.…
Chuseok22 Dec 22, 2025
a7fec07
PostgreSQL_연결_및_테이블_생성 : feat : github_cursor_repository 추가 https://g…
Chuseok22 Dec 22, 2025
8e26b45
PostgreSQL_연결_및_테이블_생성 : feat : github_cursor 테이블 flyway migration 코드…
Chuseok22 Dec 22, 2025
6ec67cd
PostgreSQL_연결_및_테이블_생성 : feat : requirements.txt 의존성 업데이트 https://git…
Chuseok22 Dec 22, 2025
3b3d060
PostgreSQL_연결_및_테이블_생성 : feat : coderabbitai.yaml 추가 https://github.c…
Chuseok22 Dec 22, 2025
c7e3c0e
PostgreSQL_연결_및_테이블_생성 : feat : env 환경변수 local, production 분리 https:/…
Chuseok22 Dec 22, 2025
7aeea4d
PostgreSQL_연결_및_테이블_생성 : feat : repository docstring 추가 https://githu…
Chuseok22 Dec 22, 2025
474b474
PostgreSQL_연결_및_테이블_생성 : feat : 코드 포맷팅 https://github.com/Team-Romi/c…
Chuseok22 Dec 22, 2025
53a8285
PostgreSQL_연결_및_테이블_생성 : feat : postgresql DB 연결 실패 시 세션 종료 로직 개선 htt…
Chuseok22 Dec 22, 2025
5fae0f8
PostgreSQL_연결_및_테이블_생성 : feat : github env api_token 추가 https://githu…
Chuseok22 Dec 22, 2025
483169e
PostgreSQL_연결_및_테이블_생성 : feat : postgres 엔티티 created_at, updated_at 및…
Chuseok22 Dec 22, 2025
3bb40da
PostgreSQL_연결_및_테이블_생성 : feat : postgres 엔티티 created_at, updated_at 및…
Chuseok22 Dec 22, 2025
9adbab9
PostgreSQL_연결_및_테이블_생성 : feat : 서버 시작 시 PG 테이블 생성 및 애플리케이션 시작 종료 로깅 h…
Chuseok22 Dec 22, 2025
f7024c5
PostgreSQL_연결_및_테이블_생성 : feat : .env 환경변수 파일 자동 업로드 workflow 추가 https…
Chuseok22 Jan 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "ko-KR"
early_access: false
reviews:
profile: "chill"
request_changes_workflow: false
high_level_summary: true
poem: true
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
base_branches:
- main
- test
chat:
auto_reply: true
27 changes: 26 additions & 1 deletion app/config/database.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,27 @@
# 빈 파일 - DB 연결 설정
from typing import AsyncGenerator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

from app.config.settings import get_settings

_settings = get_settings()

_engine = create_async_engine(
_settings.postgres_url,
pool_pre_ping=True,
)

_async_session_factory = async_sessionmaker(
bind=_engine,
expire_on_commit=False,
autoflush=False,
)


def get_async_session_factory() -> async_sessionmaker[AsyncSession]:
return _async_session_factory


async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with _async_session_factory() as session:
yield session
46 changes: 45 additions & 1 deletion app/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
# 빈 파일 - 환경 변수 설정
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
"""
.env 에서 환경변수 로딩
"""

model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)

# GitHub API 설정
github_api_base_url: str

# Ollama 설정
ollama_base_url: str
ollama_api_key: str
ollama_model: str
ollama_timeout_seconds: int

# Qdrant 설정
qdrant_base_url: str
qdrant_collection: str
qdrant_api_key: str

# 텍스트 청크 설정
text_chunk_max_chars: int
text_chunk_overlap_chars: int
text_chunk_hard_max_chars: int

# 동시성 설정
concurrency_embedding_max_concurrency: int

# PostgreSQL 설정
postgres_url: str
Comment on lines 6 to 42
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Environment variable field and .env example file check
echo "=== Settings 클래스의 필수 필드 확인 ==="
rg -A 1 "^\s+[a-z_]+:" app/config/settings.py

echo -e "\n=== .env 또는 .env.example 파일 존재 확인 ==="
fd -t f "^\.env" -H
fd -t f "\.env\.example$" -H

Repository: Team-Romi/romi-chatbot

Length of output: 506


🏁 Script executed:

# Check the full settings.py file to understand validation setup
cat -n app/config/settings.py

Repository: Team-Romi/romi-chatbot

Length of output: 1205


🏁 Script executed:

# Check for any Field imports or validation patterns
rg "Field|validator|field_validator" app/config/settings.py

Repository: Team-Romi/romi-chatbot

Length of output: 48


🏁 Script executed:

# Search for get_settings usage and error handling
rg "get_settings" --type py -B 2 -A 2

# Check main.py or app initialization
fd -t f "main\.py$|app\.py$|__main__\.py$"

Repository: Team-Romi/romi-chatbot

Length of output: 603


🏁 Script executed:

# Check for .env documentation or example in README or docs
fd -t f "README|\.md$" | head -20
rg "\.env|환경변수|environment" --type md -B 2 -A 2 | head -50

Repository: Team-Romi/romi-chatbot

Length of output: 75


🏁 Script executed:

# Check main.py for settings initialization
cat -n app/main.py

# Check claude.md for documentation
cat -n claude.md | head -100

Repository: Team-Romi/romi-chatbot

Length of output: 1177


🏁 Script executed:

# Check database.py to see how _settings is used
cat -n app/config/database.py

Repository: Team-Romi/romi-chatbot

Length of output: 885


.env.example 파일 추가 및 필드 검증 명시 필요

모든 설정 필드가 필수이며 기본값이 없지만 .env.example 파일이 존재하지 않아 개발자가 필요한 환경변수를 알 수 없습니다. 또한 Field() 검증을 통한 명시적 설명이 없습니다. .env 파일이 누락되거나 필수 환경변수가 없을 경우 database.py 모듈 임포트 시점에 Pydantic 검증 에러가 발생합니다.

.env.example 파일을 작성하여 모든 필수 환경변수와 예제값을 문서화하고, 각 필드에 Field(description="...") 을 추가하여 검증 목적을 명확히 하세요.

🤖 Prompt for AI Agents
app/config/settings.py lines 6-41: The Settings class exposes many required env
fields but lacks Field(...) descriptions and there's no .env.example to show
required variables; create a .env.example at the repo root listing every
required variable with example values and add Field(description="...") to each
Settings attribute (github_api_base_url, ollama_base_url, ollama_api_key,
ollama_model, ollama_timeout_seconds, qdrant_base_url, qdrant_collection,
qdrant_api_key, text_chunk_max_chars, text_chunk_overlap_chars,
text_chunk_hard_max_chars, concurrency_embedding_max_concurrency, postgres_url)
so Pydantic validation messages and generated docs clearly state what each env
var means and expected types; keep fields required (no defaults) or add sensible
defaults where appropriate, and update documentation/README to reference
.env.example.

Comment on lines +18 to +42
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The settings contain sensitive credentials (API keys, tokens, database URLs) but there's no validation to ensure these required security-sensitive fields are actually provided. Consider adding validation to ensure that critical security credentials like github_api_token, ollama_api_key, qdrant_api_key, and postgres_url are not empty strings or contain placeholder values when the application starts.

Copilot uses AI. Check for mistakes.


@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings()
9 changes: 9 additions & 0 deletions app/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
"""
SQLAlchemy Declarative Base
- 모든 엔티티는 Base 상속
"""
pass
Empty file added app/models/enums/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions app/models/enums/source_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from enum import Enum


class SourceType(str, Enum):
"""
깃허브 임베딩 대상 SourceType
- Repository: 레포 파일/문서 (README 등)
- ISSUE: 이슈
- PULL_REQUEST: PR
- COMMIT: 커밋
- RELEASE: 릴리즈
"""
REPOSITORY = "REPOSITORY"
ISSUE = "ISSUE"
PULL_REQUEST = "PULL_REQUEST"
COMMIT = "COMMIT"
RELEASE = "RELEASE"
29 changes: 29 additions & 0 deletions app/models/github_cursor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import uuid
from datetime import datetime

from sqlalchemy import Enum as SqlEnum
from sqlalchemy import UniqueConstraint, Index, String, DateTime, func
from sqlalchemy.dialects.postgresql.base import UUID
from sqlalchemy.orm import Mapped, mapped_column

from app.models.base import Base
from app.models.enums.source_type import SourceType


class GithubCursorEntity(Base):
__tablename__ = "github_cursor"
__table_args__ = (
UniqueConstraint("repository_name", "source_type", name="uq_github_cursor"),
Index("idx_github_cursor_repo_type", "repository_name", "source_type")
)

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

repository_name: Mapped[str] = mapped_column(String(200), nullable=False)

source_type: Mapped[SourceType] = mapped_column(SqlEnum(SourceType, native_enum=False), nullable=False)

cursor_value: Mapped[str] = mapped_column(String(500), nullable=False)

created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
2 changes: 0 additions & 2 deletions app/models/github_issue.py

This file was deleted.

45 changes: 45 additions & 0 deletions app/repositories/github_cursor_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import uuid
from typing import Optional

from sqlalchemy import select, func
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.enums.source_type import SourceType
from app.models.github_cursor import GithubCursorEntity


class GithubCursorRepository:
async def find_by_repository_name_and_source_type(
self,
session: AsyncSession,
repository_name: str,
source_type: SourceType,
) -> Optional[GithubCursorEntity]:
query = select(GithubCursorEntity).where(
GithubCursorEntity.repository_name == repository_name,
GithubCursorEntity.source_type == source_type,
)
result = await session.execute(query)
return result.scalar_one_or_none()

async def upsert(
self,
session: AsyncSession,
repository_name: str,
source_type: SourceType,
cursor_value: str,
) -> None:
query = insert(GithubCursorEntity).values(
id=uuid.uuid4(),
repository_name=repository_name,
source_type=source_type,
cursor_value=cursor_value,
).on_conflict_do_update(
index_elements=["repository_name", "source_type"],
set_={
"cursor_value": cursor_value,
"updated_at": func.now(),
},
)
await session.execute(query)
Copy link

@coderabbitai coderabbitai bot Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the GithubCursorRepository file
find . -name "*github_cursor_repository*" -type f

# Look for GithubCursorRepository usage
echo -e "\n=== GithubCursorRepository usages ==="
rg -n "GithubCursorRepository" --type py -B 2 -A 10 2>/dev/null | head -100

# Look for session management patterns in service/handler layers
echo -e "\n=== Session commit patterns ==="
rg -n "\.commit\(\)|\.execute\(" --type py -B 2 -A 2 | head -80

Repository: Team-Romi/romi-chatbot

Length of output: 1764


🏁 Script executed:

#!/bin/bash
# List all repository files to understand the pattern
find . -path "*/repositories/*.py" -type f | head -20

# Check the github_cursor_repository file specifically
echo -e "\n=== GithubCursorRepository file ==="
rg -n "class GithubCursorRepository|def upsert" --type py -B 2 -A 15 2>/dev/null

Repository: Team-Romi/romi-chatbot

Length of output: 2676


🏁 Script executed:

#!/bin/bash
# Look for session setup patterns
echo "=== Session dependency setup ==="
rg -n "get_session|get_db|AsyncSession" --type py -B 1 -A 3 | grep -E "(def|class|yield|commit)" | head -50

# Check if there's a Unit of Work or similar pattern
echo -e "\n=== Unit of Work patterns ==="
rg -n "UnitOfWork|async with.*session|SessionLocal" --type py | head -30

Repository: Team-Romi/romi-chatbot

Length of output: 439


🏁 Script executed:

#!/bin/bash
# Get the complete database.py file
echo "=== Complete database.py ==="
cat app/config/database.py

# Find all places where upsert is called
echo -e "\n=== Upsert method calls ==="
rg -n "\.upsert\(" --type py -B 3 -A 5 2>/dev/null

Repository: Team-Romi/romi-chatbot

Length of output: 754


🏁 Script executed:

#!/bin/bash
# Look for service layer that might use the repository
echo "=== Service/handler files ==="
find . -path "*/services/*.py" -o -path "*/handlers/*.py" -o -path "*/api/routes/*.py" | head -20

# Search for GithubCursor usage in services/handlers
echo -e "\n=== GithubCursor in services/handlers ==="
rg -n "GithubCursor" --type py -B 2 -A 8 | grep -v "^Binary" | head -100

Repository: Team-Romi/romi-chatbot

Length of output: 3703


🏁 Script executed:

#!/bin/bash
# Search for where upsert is actually called
echo "=== Upsert calls in github_service ==="
cat app/services/github_service.py | grep -A 10 -B 5 "upsert" || echo "Not in github_service"

# Look at github routes
echo -e "\n=== Github route handlers ==="
cat app/api/routes/github.py 2>/dev/null || echo "File not found"

# Search more broadly for upsert usage
echo -e "\n=== All upsert usages ==="
rg -n "upsert" --type py -B 5 -A 10 2>/dev/null

Repository: Team-Romi/romi-chatbot

Length of output: 1409


🏁 Script executed:

#!/bin/bash
# Check if there's middleware or any auto-commit pattern
echo "=== Middleware/app setup ==="
find . -name "main.py" -o -name "app.py" | xargs cat 2>/dev/null | head -100

# Look for any @app or FastAPI setup
echo -e "\n=== FastAPI app initialization ==="
rg -n "FastAPI\(\|@app\.|Depends" --type py -B 1 -A 3 | head -80

# Check if there's any transaction/commit handling in middleware or lifespan
echo -e "\n=== Middleware or transaction handling ==="
rg -n "middleware|lifespan|Depends.*session" --type py -B 2 -A 5 | head -100

Repository: Team-Romi/romi-chatbot

Length of output: 338


🏁 Script executed:

#!/bin/bash
# Check the complete github_cursor_repository.py file to see full context
echo "=== Complete github_cursor_repository.py ==="
cat app/repositories/github_cursor_repository.py

# Check all repository files to see the pattern
echo -e "\n=== Other repository methods (sample) ==="
rg -n "async def" app/repositories/ --type py -A 10 | head -150

Repository: Team-Romi/romi-chatbot

Length of output: 3176


🏁 Script executed:

#!/bin/bash
# Final verification: check if any caller handles session commit
echo "=== Check for session.commit or session.rollback patterns ==="
rg -n "session\.commit|session\.rollback|async with.*session" --type py | head -50

# Verify database.py doesn't have commit in finally/except
echo -e "\n=== Database.py full context (with surrounding lines) ==="
cat -n app/config/database.py

Repository: Team-Romi/romi-chatbot

Length of output: 1082


세션 트랜잭션 커밋 처리 필수

get_async_session() 의존성 함수가 세션을 yield한 후 커밋 처리가 없습니다. 현재 코드는 upsert() 메서드의 데이터 변경사항이 데이터베이스에 반영되지 않습니다.

get_async_session()을 다음과 같이 수정하세요:

async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
  async with _async_session_factory() as session:
    try:
      yield session
      await session.commit()
    except Exception:
      await session.rollback()
      raise

이는 FastAPI + SQLAlchemy 표준 패턴으로, 요청 범위 내 모든 데이터베이스 변경사항을 자동으로 커밋하고 오류 발생 시 롤백을 보장합니다.

🤖 Prompt for AI Agents
In app/repositories/github_cursor_repository.py around lines 26 to 45 the
upsert() call modifies the database but the session provider is not committing,
so changes are never persisted; update the get_async_session dependency (where
AsyncSession is yielded) to follow the FastAPI+SQLAlchemy pattern: use async
with _async_session_factory() as session: yield session; after the yield call
await session.commit(); and in an except block await session.rollback() then
re-raise, ensuring every request scope auto-commits on success and rolls back on
error so upsert() persists its changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai get_async_session()에서 매번 commit()을 수행하면 transaction 경계를 잡기 힘들지 않아??
spring boot 에서는 어노테이션을 통해서 트랜잭션을 관리할 수 있는데 python fastapi는 주로 어떤식으로 트랜잭션을 관리해??

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

FastAPI SQLAlchemy async transaction management patterns best practices

💡 Result:

Short practical patterns and best practices for async transaction management with FastAPI + SQLAlchemy:

  1. Use short-lived AsyncSession per request (dependency). Create async_sessionmaker once, yield a session from a dependency, and ensure rollback on exceptions and close after use. This avoids shared/global sessions. [3][5]

  2. Use explicit transaction context managers. Wrap multi-step DB work in AsyncSession.begin() (async with session.begin(): ...) so commit/rollback is automatic and safe in async code. For single-statement reads you can still use the session without begin(). [1][2]

  3. Savepoints / nested transactions for isolation in libraries/tests. Use begin_nested() (SAVEPOINT) when you need rollback inner units without aborting outer transaction (also useful in tests). For tests prefer binding sessions to a connection + top-level transaction and use savepoints to rollback between tests. [1][4]

  4. Configure session options for async usage:

    • use async engine (create_async_engine) with asyncpg for Postgres,
    • create async_sessionmaker(..., class_=AsyncSession, expire_on_commit=False) to avoid surprises with expired objects after commit,
    • in tests use join_transaction_mode="create_savepoint" if you want commits inside tests but still roll back outer transaction. [3][4]
  5. Error handling: always catch exceptions in the dependency or transactional scope, await session.rollback() before re-raising, and avoid swallowing DB errors. Rely on context managers for most cases. [3][5]

  6. Avoid long-lived transactions and large transactional work in request handlers. Keep transactions short (do heavy work outside transaction or in background jobs) to reduce lock contention and connection exhaustion. [1][6]

  7. Connection-pool and driver considerations:

    • tune pool size for async workers (uvicorn/gunicorn) and pgbouncer mode (transaction vs session) — prefer transaction pooling with short sessions,
    • handle transient connection errors with retries/backoff at the engine/connection level or via retry wrappers. Monitor reconnect behavior on graceful shutdown. [5][3]
  8. Layering & testing:

    • put DB logic in repository/service layers that accept a session (dependency), so transactional boundaries are explicit and testable,
    • for integration tests create an AsyncConnection, begin a top-level transaction, bind sessions to it and rollback at test end. [4][6]
  9. When mixing sync and async code, avoid running sync DB calls in the async event loop — use async drivers and AsyncSession throughout, or run sync code in threadpool deliberately. [1]

Short checklist to implement in FastAPI:

  • create_async_engine(DATABASE_URL) with asyncpg

  • async_sessionmaker(..., class_=AsyncSession, expire_on_commit=False)

  • dependency:

    async def get_session():
    async with async_sessionmaker() as session:
    try:
    yield session
    except:
    await session.rollback()
    raise

  • use async with session.begin(): for transactional endpoints

  • use begin_nested() for savepoints where needed

  • tune pool and join_transaction_mode for tests

Sources: SQLAlchemy transaction docs and contextual sessions; FastAPI community patterns; test fixtures / savepoint examples; common pitfalls discussion. [1][2][3][4][5]

References
[1] SQLAlchemy — Transactions and Connection Management (Async-aware).
[2] SQLAlchemy — Contextual / session transaction docs.
[3] FastAPI community discussion: async_sessionmaker per-request dependency example.
[4] Test fixture example: AsyncConnection + savepoint pattern (gist).
[5] StackOverflow / GH issues on middleware/global-session pitfalls and pool/commit behavior.


@Chuseok22 좋은 질문입니다! 맞습니다. get_async_session()에서 매번 자동 커밋하면 복잡한 트랜잭션 경계 관리가 어려워집니다.

FastAPI의 트랜잭션 관리 패턴

Spring Boot의 @Transactional과 달리, FastAPI/Python에는 표준 어노테이션 기반 트랜잭션 관리가 없습니다. 대신 여러 패턴을 사용할 수 있습니다:

1. 명시적 트랜잭션 블록 (권장)

세션 의존성은 커밋 없이 세션만 제공하고, 서비스/라우터 레이어에서 명시적으로 트랜잭션을 관리:

# app/config/database.py
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    async with _async_session_factory() as session:
        yield session
        # 커밋하지 않음 - 호출자가 결정
# 서비스 레이어에서 명시적 트랜잭션 관리
async def update_github_data(
    repository_name: str,
    session: AsyncSession = Depends(get_async_session)
):
    async with session.begin():  # 트랜잭션 시작
        # 여러 repository 작업을 하나의 트랜잭션으로 묶음
        await github_cursor_repo.upsert(session, repository_name, "ISSUE", "cursor1")
        await github_cursor_repo.upsert(session, repository_name, "PR", "cursor2")
        # 블록 종료 시 자동 커밋, 예외 발생 시 자동 롤백

장점:

  • 트랜잭션 경계를 명시적으로 제어
  • 여러 작업을 하나의 트랜잭션으로 묶기 쉬움
  • Spring의 @Transactional과 유사한 제어 수준

2. 의존성 레벨 자동 커밋

단순한 CRUD 애플리케이션에 적합:

async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
    async with _async_session_factory() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise

장점: 간단한 케이스에서 보일러플레이트 감소
단점: 복잡한 트랜잭션 경계 관리 어려움

3. Unit of Work 패턴

더 복잡한 도메인 로직을 위한 패턴:

class UnitOfWork:
    def __init__(self, session: AsyncSession):
        self.session = session
        self.github_cursor_repo = GithubCursorRepository()
    
    async def __aenter__(self):
        await self.session.begin()
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            await self.session.rollback()
        else:
            await self.session.commit()

# 사용
async with UnitOfWork(session) as uow:
    await uow.github_cursor_repo.upsert(...)

현재 코드에 대한 권장사항

현재 구조를 고려하면 **패턴 1 (명시적 트랜잭션 블록)**을 권장합니다:

  1. get_async_session()은 세션만 제공 (커밋 없음)
  2. 단순 조회는 그냥 사용
  3. 쓰기 작업은 async with session.begin(): 블록으로 감싸기

이렇게 하면:

  • 단순 케이스에서는 간단하게 사용
  • 복잡한 트랜잭션도 명시적으로 제어 가능
  • 나중에 여러 repository 작업을 묶기 쉬움

현재 upsert() 메서드가 변경사항을 영구 저장하려면, 호출하는 곳에서 session.begin() 블록을 사용하거나 명시적으로 await session.commit()을 호출해야 합니다.

참고로, SQLAlchemy 2.0+의 async with session.begin()은 Spring의 @Transactional과 매우 유사한 선언적 트랜잭션 관리를 제공합니다.

36 changes: 34 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
alembic==1.17.2
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.0
asyncpg==0.31.0
certifi==2025.11.12
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
fastapi==0.124.4
cryptography==46.0.3
Deprecated==1.3.1
fastapi==0.127.0
greenlet==3.3.0
grpcio==1.76.0
grpcio-tools==1.76.0
h11==0.16.0
h2==4.3.0
hpack==4.1.0
httpcore==1.0.9
httpx==0.28.1
hyperframe==6.1.0
idna==3.11
Mako==1.3.10
MarkupSafe==3.0.3
numpy==2.4.0
portalocker==3.2.0
protobuf==6.33.2
pycparser==2.23
pydantic==2.12.5
pydantic-settings==2.12.0
pydantic_core==2.41.5
PyGithub==2.8.1
PyJWT==2.10.1
PyNaCl==1.6.1
python-dotenv==1.2.1
qdrant-client==1.16.2
requests==2.32.5
setuptools==80.9.0
SQLAlchemy==2.0.45
starlette==0.50.0
typing-inspection==0.4.2
typing_extensions==4.15.0
uvicorn==0.38.0
urllib3==2.6.2
uvicorn==0.40.0
wrapt==2.0.1
Comment on lines +1 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

의존성 버전 호환성 검증 및 테스트 필요

FastAPI 0.127.0과 SQLAlchemy 2.0.45, asyncpg 0.31.0의 조합에 대한 호환성 검증이 필요합니다. 특히 asyncpg 0.31.0은 SQLAlchemy 2.0에서 권장되는 버전 범위(0.29.0 이상)를 초과하므로, 프로덕션 배포 전에 데이터베이스 쿼리와 비동기 세션 동작을 철저히 테스트하시기 바랍니다.

🤖 Prompt for AI Agents
In requirements.txt lines 1-45, the combination FastAPI 0.127.0, SQLAlchemy
2.0.45 and asyncpg 0.31.0 may have compatibility risks; run compatibility and
integration tests (unit + DB async integration) against asyncpg 0.31.0 and
SQLAlchemy 2.0.45 to confirm async session/query behavior; if tests fail, change
the asyncpg version to a tested range (e.g., pin to a supported version such as
>=0.29.0,<0.31.0 or the specific version that passes tests) and update
requirements.txt accordingly, then re-run CI matrix including migrations and
real DB connections to validate the fix.

16 changes: 16 additions & 0 deletions sql/V20251222_141233__create_github_cursor_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS github_cursor (
id UUID PRIMARY KEY,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

UUID PRIMARY KEY에 DEFAULT 값이 누락되었습니다.

id 컬럼에 기본값이 없어 INSERT 시 명시적으로 UUID를 제공해야 합니다. PostgreSQL에서는 자동 생성을 위해 DEFAULT를 추가하는 것이 일반적입니다.

🔎 제안하는 수정
-    id UUID PRIMARY KEY,
+    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

참고: gen_random_uuid()는 PostgreSQL 13+에서 기본 제공됩니다. 이전 버전을 사용하는 경우 uuid-ossp 확장의 uuid_generate_v4()를 사용하세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id UUID PRIMARY KEY,
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
🤖 Prompt for AI Agents
In sql/V20251222_141233__create_github_cursor_table.sql around line 2, the id
UUID PRIMARY KEY column lacks a DEFAULT value so inserts must always supply a
UUID; update the column definition to set a DEFAULT that generates a UUID (e.g.,
DEFAULT gen_random_uuid() for PG 13+, or DEFAULT uuid_generate_v4() if using the
uuid-ossp extension) and, if using uuid_generate_v4(), ensure the uuid-ossp
extension is created in the migration prior to table creation.

repository_name VARCHAR(200) NOT NULL,
source_type VARCHAR(50) NOT NULL,
cursor_value VARCHAR(500) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

updated_at 자동 업데이트 메커니즘이 누락되었습니다.

updated_at 컬럼은 DEFAULT NOW()로 생성 시에만 설정되며, UPDATE 시 자동으로 갱신되지 않습니다. 트리거를 추가하거나 애플리케이션 레벨에서 처리해야 합니다.

🔎 제안하는 수정 (트리거 추가)

테이블 생성 후 다음 트리거를 추가하세요:

-- updated_at 자동 업데이트 함수 생성
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- 트리거 생성
CREATE TRIGGER update_github_cursor_updated_at
    BEFORE UPDATE ON github_cursor
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
🤖 Prompt for AI Agents
In sql/V20251222_141233__create_github_cursor_table.sql around line 7, the
updated_at column only has DEFAULT NOW() and won’t auto-refresh on UPDATE; add
an automatic update mechanism by creating a plpgsql trigger function that sets
NEW.updated_at = NOW() and then create a BEFORE UPDATE trigger on github_cursor
to call that function (alternatively, document that the application must
explicitly set updated_at on updates if you prefer app-level handling).

Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQL schema lacks an automatic update trigger for the updated_at column. Currently, updated_at only has a DEFAULT NOW() which sets it on INSERT but won't automatically update on modifications. Consider adding a trigger to automatically update this timestamp on UPDATE operations, or ensure it's explicitly updated in application code.

Copilot uses AI. Check for mistakes.
CONSTRAINT uq_github_cursor UNIQUE (repository_name, source_type),

CONSTRAINT ck_github_cursor_source_type
CHECK (source_type IN ('REPOSITORY', 'ISSUE', 'PULL_REQUEST', 'COMMIT', 'RELEASE'))
);

-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_github_cursor_repo_type
ON github_cursor (repository_name, source_type);
Comment on lines +12 to +16
Copy link

Copilot AI Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index idx_github_cursor_repo_type is redundant because it's created on the same columns as the unique constraint uq_github_cursor. PostgreSQL automatically creates an index for unique constraints, so this separate index is unnecessary and wastes storage space. Remove this index definition or, if you need it for performance reasons in specific query patterns, add a comment explaining why.

Suggested change
);
-- 인덱스 생성
CREATE INDEX IF NOT EXISTS idx_github_cursor_repo_type
ON github_cursor (repository_name, source_type);
);

Copilot uses AI. Check for mistakes.