본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

열을 주입하고 스키마는 그대로 둬

~14 min · sql, schema, migration, implementation

Level 0흩어진 부품
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

같은 표를 각자 써놨던 엔진 셋

작업장 셋이 독립적으로 있었어. 문서에 대한 일을 줄 세우는 것, 영상 에피소드에 대한 일을 줄 세우는 것, 기억 항목에 대한 일을 줄 세우는 것. 각각이 같은 표들을 길렀어. 큐 행, 작업자 둘이 같은 일을 못 집게 하는 선점, 결과 둘이 동시에 못 내려앉게 하는 리스, 로그.

구조는 똑같아. 글자로는 딱 한 군데가 중요하게 달랐어. 각자 일의 주체를 자기 도메인 이름으로 불렀거든. 그리고 각각이 이미 진짜 행이 들어 있는 살아 있는 데이터베이스를 갖고 있었고, 자기 인덱스 이름을 갖고 있었어.

그 사소함이 추출을 거의 죽일 뻔한 이유

공용 빌더가 어휘 하나를 강요했으면 채택은 작업장마다 데이터 이주를 뜻했을 거야. 열 이름 바꾸고, 인덱스 다시 만들고, 살아 있는 행 옮기고, 옛 이름을 참조하는 데가 없다고 확신하고. 비싸고 위험하고, 무엇보다 추출을 아예 시도 안 하게 만드는 종류의 값이야. 이주가 필요한 리팩터링은 일정에 잡히고, 개인 프로젝트에서 일정에 잡힌 일은 안 일어나는 일이야.

그래서 빌더가 열 이름을 인자로 받고 CREATE TABLE IF NOT EXISTS 를 뱉어. 결과를 정확히 말할 값이 있어. 기존 데이터베이스는 절대 안 바뀌어. 이미 있는 표에 대고는 그 문장이 아무것도 안 하니까. 새 데이터베이스는 정식 모양을 받고. 테스트 픽스처든 새 작업장이든. 채택이 저장소 변경 0 인 코드 옮기기가 돼. 그래서 셋 다 하루 만에 채택하고 기존 테스트로 증명할 수 있었어.

채택해도 데이터가 안 바뀌게 추출을 설계해. 통합의 제일 큰 숨은 값은 코드인 적이 거의 없어. 그 코드가 이미 적어둔 것이지. 임포트만 고쳐서 채택할 수 있는 추출은 채택돼. 이주가 필요한 건 기능 작업이랑 경쟁하고, 지고, 다들 고쳐야 한다고 동의하는 채로 중복이 또 일 년을 살아남아.

열 위치도 계약의 일부야

깐깐함으로 치부하기 쉬운 디테일이 하나 더 있어. 빌더는 앱마다의 추가 열을 그 앱의 살아 있는 표가 이미 갖고 있는 위치에 끼워. 전부 끝에 붙이는 게 아니라.

이게 중요한 건 새 테스트 데이터베이스랑 운영 데이터베이스가 똑같은 행 모양을 내야 하기 때문이야. 행을 위치로 인덱싱하는 코드, 순서대로 행을 만드는 테스트 픽스처, 열을 찍는 진단 — 이 중 뭐든 새 빌더가 만든 표랑 몇 년 전에 만들어진 표에서 다르게 굴 수 있어. 위치를 맞춘다는 건 새 데이터베이스에 대고 쓴 테스트가 진짜 데이터베이스에 대한 증거가 된다는 뜻이고, 그게 애초에 테스트를 돌리는 이유 전부야.

Code

빌더 하나, 살아 있는 데이터베이스 셋, 이주 0·python
def delegations_ddl(subject_col: str,
                    extra_cols: tuple[str, ...] = (),
                    review_cols: bool = True,
                    tail_cols: tuple[str, ...] = ()) -> str:
    """The queue row, with the app's own vocabulary injected.

    `subject_col`: what a job is ABOUT. The kernel never learns it.
    `extra_cols` : full column definitions slotted after `pipeline`,
                   at the position each live table already has them.
    `tail_cols`  : columns that ride after `commit_sha`.
    `review_cols`: False for the workshop that has no reviewer
                   machinery at all - see the next lesson.

    IF NOT EXISTS is load-bearing: an existing database is never
    altered, so adoption is code motion, not a data migration.
    """
    review = (
        "  review_on INTEGER NOT NULL DEFAULT 1,\n"
        "  review_brain TEXT,\n"
        if review_cols else ""
    )
    extras = "".join(f"  {col},\n" for col in extra_cols)
    tail = "".join(f",\n  {col}" for col in tail_cols)
    return (
        "CREATE TABLE IF NOT EXISTS delegations (\n"
        "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
        f"  {subject_col} TEXT NOT NULL,\n"
        "  pipeline TEXT NOT NULL,\n"
        f"{extras}"
        "  template_version INTEGER NOT NULL DEFAULT 0,\n"
        "  brief_path TEXT NOT NULL,\n"
        "  main_brain TEXT NOT NULL,\n"
        f"{review}"
        "  notes TEXT NOT NULL DEFAULT '',\n"
        "  status TEXT NOT NULL DEFAULT 'queued',\n"
        "  created_at TEXT NOT NULL,\n"
        "  taken_at TEXT,\n"
        "  taken_by TEXT,\n"
        "  landed_at TEXT,\n"
        f"  commit_sha TEXT{tail}\n"
        ");\n"
    )


def claims_ddl(subject_col: str, index_name: str) -> str:
    """One active claim per subject. The PARTIAL UNIQUE INDEX is the
    race-free guarantee - the database refuses a second open claim, so
    no application-level lock is needed. `index_name` keeps each app's
    existing index so adoption creates no duplicate on a live DB."""
    return (
        "CREATE TABLE IF NOT EXISTS claims (\n"
        "  id INTEGER PRIMARY KEY AUTOINCREMENT,\n"
        f"  {subject_col} TEXT NOT NULL,\n"
        "  session_id TEXT NOT NULL,\n"
        "  acquired_at TEXT NOT NULL,\n"
        "  released_at TEXT\n"
        ");\n"
        f"CREATE UNIQUE INDEX IF NOT EXISTS {index_name}\n"
        f"  ON claims({subject_col}) WHERE released_at IS NULL;\n"
    )


# Each app's shim, in one line each:
#   documents_schema = delegations_ddl("doc_slug")
#   episodes_schema  = delegations_ddl("slug",
#                          extra_cols=("pointer TEXT NOT NULL DEFAULT '{}'",),
#                          tail_cols=("render_path TEXT",))
#   memory_schema    = delegations_ddl("vault", review_cols=False)

External links

Exercise

네 세계에서 도메인 이름만 다르고 거의 똑같은 표를 가진 서비스 둘을 찾아. 둘 다 만들어낼 빌더 함수를 써. 다른 이름들을 인자로 받고. 그다음 진짜 질문에 답해. 각 서비스가 이주 없이 이걸 채택할 수 있어? 못 하면 어떤 차이가 이주를 강제하는지 정확히 나열해. 그것들이 코드를 쓰기 전에 협상해서 없앨 값이 있는 것들이야.
Hint
열 이름뿐 아니라 열 순서도 비교해. 같은 열이 다른 순서로 있는 표 둘은 위치에 의존하는 모든 것에서 다른 결과를 내고, 그 차이는 출력을 정렬하는 스키마 diff 에선 안 보여.

Progress

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

댓글 0

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

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