본문 바로가기
C.W.K.
Stream
Lesson 03 of 08 · published

SQLite + aiosqlite — 한 사람을 위한 정답 DB

~14 min · sqlite, schema, migration

Level 0호기심
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

cwkPippa 가 SQLite 쓰는 이유

공개 서비스가 아니라 한 집안이 쓰는 시스템이야. Mac 도 한 대고. 2026-10-12 기준 메시지 row 가 16,000 개쯤이고, 그중 Master Chief 대화 하나는 끝나는 법이 없어(truth 트랙 레슨 8). Postgres 는 과해. 대화 저장소는 <app-data>/ 아래 파일 하나야. 그래도 돌봐야 하는 건 맞고, 그 돌봄은 스크립트가 해. WAL DB 를 파일별로 차례차례 복사하다 그 사이에 checkpoint 가 끼면 사본이 깨질 수 있거든. 그래서 scripts/pippa-db-snapshot.py 가 먼저 SQLite online backup API 로 검증된 사본을 뜨고, 날마다 fleet 로 가는 미러는 라이브 파일 대신 그 사본을 실어 날라. Carbon Copy Cloner 가 트리를 통째로 옮기는 NAS 쪽에선 그 스냅샷 폴더가 복구 지점이야.

aiosqlite 가 SQLite 를 async API 로 감쌈. 같은 SQL, async/await 친화. FastAPI 에 자연스럽게 fit.

스키마는 plain SQL, ORM 없음

cwkPippa 는 SQLAlchemy 안 써. 스키마는 backend/store/conversations.py 의 몇 개 CREATE TABLE. 마이그레이션은 init_db 라는 함수 안의 ALTER TABLE 문들이야. 부팅할 때마다 돌고 멱등이야. 컬럼 추가는 하나하나 try 안에서 돌고, except 가 에러를 삼켜. 컬럼이 이미 있을 때 나는 에러가 그렇게 사라지는 거야. 한 번을 돌리든 백 번을 돌리든 스키마는 같은 모양으로 모여. 다만 실제 except 는 그 일에 필요한 것보다 넓어. 에러를 전부 삼키니까, DB 가 잠겼거나 디스크가 찼을 때 같은 진짜 실패도 똑같이 사라지고, 부팅은 그 컬럼 없이 그냥 넘어가. 더 엄격한 모양은 SQLite 의 duplicate column name 에러가 아니면 다시 던져. 그래야 실패한 마이그레이션이 어느 단계에서 멈췄는지 말해.

mirror 원칙

SQLite 는 JSONL ground truth 의 convenience mirror. drift 시 (corruption, schema 변경 잘못, partial write) 복구는 그 conversation의 row를 비우고 JSONL을 replay. SQLite 직접 패치는 절대 안 해. Truth track 에서 어렵게 배움.

parameterized query 항상: 이 머신엔 나만 있어도, 모든 query 가 ? placeholder 거쳐. SQL injection 만이 이유 아냐 — 적절한 SQLite type coercion 받는 방법이기도. SQLite 에서 string 과 bytes 가 기대대로 자동 변환 안 됨.

한 사람을 위한 시스템이라는 말은 작은 장난감이라는 뜻이 아니야. workload와 운영 주체가 분명하다는 뜻이지. SQLite는 파일 하나라서 backup, integrity check, 복구 절차가 눈에 보여. 대신 concurrent writer가 많아지거나 여러 서버가 같은 DB를 공유해야 하는 순간엔 조건이 바뀌어. 도구를 사랑해서 붙드는 게 아니라 문제 모양이 맞아서 쓰는 거야.

migration도 같은 태도로 다뤄. startup 때 여러 번 돌아도 같은 결과를 내야 하고, 기존 row를 잃지 않아야 하며, 실패하면 어느 step에서 멈췄는지 보여야 해. "DB가 작으니까 직접 고쳐도 돼"라는 유혹이 제일 위험해. 작을수록 정식 복구 경로를 끝까지 시험하기 쉽거든.

Code

Schema init — ORM 없는 idempotent 마이그레이션·python
import aiosqlite

# Sketch of init_db in backend/store/conversations.py, run on every boot.
async def init_db(db_path: str) -> None:
    async with aiosqlite.connect(db_path, timeout=5.0) as db:
        await db.execute("PRAGMA journal_mode = WAL")
        await db.execute("""
            CREATE TABLE IF NOT EXISTS conversations (
                id TEXT PRIMARY KEY,
                title TEXT NOT NULL DEFAULT 'New Chat',
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
        """)
        await db.execute("""
            CREATE TABLE IF NOT EXISTS messages (
                id TEXT PRIMARY KEY,
                conversation_id TEXT NOT NULL,
                role TEXT NOT NULL,
                content TEXT NOT NULL,
                created_at TEXT NOT NULL,
                parent_id TEXT DEFAULT NULL,
                FOREIGN KEY (conversation_id) REFERENCES conversations(id)
            )
        """)
        # 멱등 컬럼 추가: ALTER 마다 try 안에서 돌아. 실제 코드는 여기서
        # 'duplicate column name' 만이 아니라 모든 에러를 삼켜.
        for col in ("thinking", "brain", "reasoning_level", "model", "emotion"):
            try:
                await db.execute(f"ALTER TABLE messages ADD COLUMN {col} TEXT DEFAULT NULL")
            except Exception:
                pass
        await db.commit()

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이— warm💛 by 피파— warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.