sqlite-utils 4.0, 이제 데이터베이스 스키마 마이그레이션(database schema migrations) 지원
요약
sqlite-utils 4.0이 출시되어 데이터베이스 스키마 마이그레이션, 중첩 트랜잭션, 복합 외래 키 지원을 새롭게 도입했습니다. 특히 Python을 통해 스키마 변경 사항을 정의하고 관리할 수 있는 강력한 기능을 제공합니다.
핵심 포인트
- 데이터베이스 스키마 마이그레이션 기능 공식 지원
- db.atomic() 메서드를 통한 중첩 트랜잭션 구현 가능
- 복합 외래 키(compound foreign keys) 지원 추가
- table.transform()을 이용한 강화된 테이블 수정 기능 제공
sqlite-utils 4.0, 이제 데이터베이스 스키마 마이그레이션(database schema migrations) 지원
2026년 7월 7일
오늘 아침 저는 해당 프로젝트의 124번째 릴리스이자 2020년 11월 3.0 이후 첫 번째 메이저 버전 업데이트인 sqlite-utils 4.0을 출시했습니다. 작지만 중요한 몇 가지 파괴적 변경 사항(이 업그레이드 가이드에 설명됨) 외에도, 이번 버전에서는 세 가지 주요 기능인 데이터베이스 마이그레이션 (database migrations), 중첩 트랜잭션 (nested transactions) (새로운 db.atomic() 메서드를 통해 제공), 그리고 복합 외래 키 (compound foreign keys) 지원이 도입되었습니다.
sqlite-utils를 사용한 데이터베이스 스키마 마이그레이션
스키마 마이그레이션(Schema migrations)은 SQLite 데이터베이스에 적용할 변경 사항의 순서를 정의하며, 어떤 마이그레이션이 적용되었는지 추적하고 아직 적용되지 않은 마이그레이션을 적용하는 메커니즘을 포함합니다.
마이그레이션은 sqlite-utils Python 라이브러리를 사용하여 Python 파일에 정의됩니다. 이 라이브러리에는 SQLite의 ALTER TABLE 문에서 지원하지 않는 강화된 테이블 수정(alter table) 기능을 제공하는 강력한 table.transform() 메서드가 포함되어 있습니다.
(table.transform()은 SQLite 문서에서 권장하는 패턴을 구현합니다. 즉, 새로운 스키마를 가진 새 임시 테이블을 생성하고, 데이터를 복사한 다음, 기존 테이블을 삭제하고 임시 테이블의 이름을 기존 이름으로 변경하는 방식입니다.)
다음은 creatures라는 테이블을 생성하고, 두 번째 단계에서 컬럼을 하나 추가하며, 세 번째 단계에서 두 개 컬럼의 타입을 변경하는 마이그레이션 파일 예시입니다:
from sqlite_utils import Migrations
migrations = Migrations("creatures")
@migrations()
def create_table(db):
db["creatures"].create(
{"id": int, "name": str, "species": str},
pk="id",
)
@migrations()
def add_weight(db):
db["creatures"].add_column("weight", float)
@migrations()
def change_column_types(db):
db["creatures"].transform(types={"species": int, "weight": str})
이를 migrations.py로 저장하고 다음과 같이 새로운 데이터베이스에 실행하십시오:
uvx sqlite-utils migrate data.db migrations.py
그 다음 해당 데이터베이스의 스키마를 확인하면:
uvx sqlite-utils schema data.db
다음과 같은 SQL을 볼 수 있습니다:
CREATE TABLE "_sqlite_migrations" (
"id" INTEGER PRIMARY KEY,
"migration_set" TEXT,
...
_sqlite_migrations 테이블은 어떤 마이그레이션 (migration) 함수들이 실행되었는지 추적하는 데 사용됩니다. 위에서 언급한 creatures 테이블은 세 가지 마이그레이션이 모두 적용된 후의 스키마 (schema)입니다.
대기 중인 마이그레이션과 이미 적용된 마이그레이션 목록을 모두 확인하려면 다음을 실행하세요:
uvx sqlite-utils migrate data.db migrations.py --list
출력 결과:
Migrations for: creatures
Applied:
create_table - 2026-07-07 17:58:41.360051+00:00
...
마이그레이션 파일을 지정하지 않으면, sqlite-utils migrate data.db 명령어가 현재 디렉토리와 하위 디렉토리를 스캔하여 migrations.py라는 이름의 파일을 찾고, 그 안에서 발견되는 모든 Migrations() 인스턴스를 적용합니다.
또한 migrations.apply(db) 메서드를 사용하여 Python 코드에서 마이그레이션을 실행할 수도 있습니다. 이는 여러 버전에 걸쳐 자체 데이터베이스 스키마를 관리하는 도구를 구축할 때 유용합니다. 저의 LLM 도구 또한 llm/embeddings_migrations.py에서 볼 수 있듯이, 몇 년 동안 이 패턴의 버전을 사용해 왔습니다.
기존 사례 (Prior art)
이 패턴에 대해 제가 가장 좋아하는 구현체는 Andrew Godwin이 그의 이전 프로젝트인 South를 기반으로 개발한 Django의 Migrations입니다. 재미있는 사실은, Andrew, Russ Keith-Magee, 그리고 제가 2008년 첫 번째 DjangoCon의 Schema Evolution 패널에서 Django를 위한 스키마 마이그레이션 경쟁 방식들을 발표했다는 점입니다! 당시 저의 시도는 런던의 Global Radio 팀과 함께 개발한 dmigrations였습니다.
Django의 마이그레이션은 모델 정의 (model definitions)로부터 자동으로 생성될 수 있으며, 이전 버전으로 롤백 (roll back) 할 수 있는 기능을 포함합니다. sqlite-utils의 접근 방식은 의도적으로 더 단순합니다. Django와 달리 sqlite-utils는 모델 정의 ORM (Object-Relational Mapping)보다는 프로그래밍 방식의 테이블 생성을 권장하므로, 마이그레이션을 자동으로 생성하는 데 사용할 수 있는 요소가 따로 없습니다.
제 경험상 롤백 (rollback)은 거의 사용되지 않는 기능이기에 제외하기로 결정했습니다. SQLite 프로젝트의 경우, 마이그레이션을 적용하기 전에 데이터베이스 파일의 복사본을 만들어 두는 것이 롤백을 달성하는 쉬운 방법입니다!
sqlite-migrate로부터의 마이그레이션
sqlite-utils 마이그레이션 설계는 이제 3년이 되었습니다. 원래 저는 이를 sqlite-migrate라는 별도의 패키지로 출시했으나, 이는 베타 릴리스 단계를 넘어 제대로 발전하지 못했습니다.
저는 해당 패키지를 충분히 많은 곳에서 사용해 왔기에 그 설계에 확신을 갖게 되었고, 성장하는 sqlite-utils/Datasette/LLM 생태계 내의 다른 모든 도구들이 기본적으로 사용할 수 있도록 sqlite-utils의 기능으로 승격시키기로 결정했습니다.
저는 sqlite-migrate의 마지막 릴리스를 진행했으며, 이 릴리스는 sqlite-utils>=4에 의존하도록 전환하고 __init__.py 파일을 다음과 같이 교체합니다:
from sqlite_utils import Migrations
all = ["Migrations"]
sqlite-migrate에 의존하는 기존 프로젝트는 변경 없이 계속 작동할 것입니다.
sqlite-utils 4.0의 그 외 모든 것
다음은 이번 버전의 릴리스 노트이며, 일부 인라인 주석이 포함되어 있습니다:
4.0 릴리스에는 몇 가지 사소한 하위 호환성 깨짐 (backwards-incompatible) 수정 사항이 포함되어 있으며 (이로 인해 메이저 버전 번호가 올라갔습니다), 세 가지 주요 새로운 기능을 도입합니다:
- 데이터베이스 마이그레이션 (Database migrations): 프로젝트의 스키마 (schema)를 시간이 지남에 따라 진화시킬 수 있는 구조화된 메커니즘을 제공합니다. (#752)
저는 마이그레이션을 이번 블로그 포스트의 핵심인 상징적인 새로운 기능이라고 생각합니다.
db.atomic()을 통한 중첩 트랜잭션 (Nested transaction) 지원, 그리고 라이브러리 전반에 걸친 트랜잭션 작동 방식의 수많은 개선 사항. (#755)
sqlite-utils는 오랫동안 데이터베이스 트랜잭션 (transactions)과 혼란스러운 관계를 맺어 왔는데, 이는 부분적으로 제가 2018년에 라이브러리 설계를 시작했을 당시 SQLite 자체에서 트랜잭션이 어떻게 작동하는지에 대해 아직 깊은 이해가 없었기 때문입니다.
코어 라이브러리에 마이그레이션을 추가하면서, 저는 마침내 이 문제를 해결하기로 결심했습니다. 트랜잭션은 마이그레이션 시스템을 훨씬 더 안전하게 만들고 추론하기 쉽게 만들어 주기 때문입니다.
저는 결국 다음과 같은 형태의 db.atomic() 컨텍스트 매니저 (context manager)를 중심으로 이를 구축하게 되었습니다:
with db.atomic():
db.table("dogs").insert({"id": 1, "name": "Cleo"}, pk="id")
db.table("dogs").insert({"id": 2, "name": "Pancakes"})
SQLite는 세이브포인트 (Savepoints)를 지원하며, 그 결과 db.atomic()은 트랜잭션 내부에서 트랜잭션을 수행할 수 있도록 중첩 (nested)될 수 있습니다. 정말 멋진 기능입니다!
table.foreign_keys를 통한 생성, 변환 및 조사를 포함하여 복합 외래 키 (compound foreign keys) 지원. (#594)
이 기능은 제가 코딩 에이전트 (coding agent)에게 4.0 릴리스에 포함되어야 할 모든 오픈 이슈 (open issues)와 PR (pull requests)을 검토해 달라고 요청했을 때 구현되었습니다. 나중에 추가할 경우 파괴적 변경 (breaking changes)이 될 수 있는 항목들을 찾아달라고 했는데, 에이전트는 복합 외래 키가 정확히 그러한 종류의 기능임을 올바르게 식별해 냈습니다.
저는 우선 table.foreign_keys 조사 (introspection) 메서드에 대한 파괴적 변경부터 시작했고, 그 후 Claude 3.5 Sonnet (Claude Fable 5로 추정되는 원문 오기)이 라이브러리에 복합 외래 키 생성 기능을 통합하는 더 까다로운 작업을 처리할 수 있을지 확인해 보기로 했습니다. 에이전트가 설계하는 데 도움을 준 API 디자인은 라이브러리의 나머지 부분과 작동 방식이 일치하여 저에게 매우 적절하게 느껴졌습니다.
다른 주목할 만한 변경 사항은 다음과 같습니다:
- 업서트 (Upserts)는 이제 SQLite의
INSERT ... ON CONFLICT ... DO UPDATE SET구문을 사용하며, 기존 테이블의 기본 키 (primary keys)를 자동으로 감지하고 필수 기본 키 값이 누락된 레코드는 거부합니다. (#652)
이 변경 사항은 제가 처음으로 파괴적 변경을 포함한 4.0 버전 업그레이드를 고려하게 만든 계기였습니다. 저는 삽입, 업데이트 또는 삭제된 테이블 내의 행 (rows)을 추적하기 위해 트리거 (triggers)를 사용하는 sqlite-chronicle을 지원하기 위해 이 기능을 구축했습니다.
db.query()는 이제 즉시 실행되며 행을 반환하지 않는 문 (statements)은 거부합니다. 쓰기 (writes) 및 DDL 작업에는 db.execute()를 사용하십시오.
아마도 가장 파괴적인 파괴적 변경 사항일 것입니다. 그 결과 저의 코드 중 몇 군데를 db.query()에서 db.execute()로 전환하도록 업데이트해야 했습니다.
- CSV 및 TSV 임포트(import) 시 이제 기본적으로 컬럼 유형(column types)을 감지하며, 기존 테이블에 데이터를 삽입(insert)할 때는 해당 테이블의 컬럼 유형을 유지합니다. (#679)
sqlite-utils insert data.db creatures creatures.csv --detect-types
플래그는 CSV 내의 데이터를 기반으로 컬럼 유형(text, integer, real)을 자동으로 감지할 수 있도록 나중에 추가된 기능이었습니다. 이것은 기본값이 되어야 하며, 4.0 버전을 출시함으로써 이를 기본값으로 설정할 수 있게 되었습니다.
table.extract()
및
extracts=
는 더 이상 모든 값이 null인 경우에 대해 조회 테이블(lookup table) 레코드를 생성하지 않습니다. (#186)
이번 릴리스에서 해결된 가장 오래된 이슈는—그 근본적인 버그는 (저에 의해) 2020년 10월에 오픈되었습니다.
하위 호환성이 깨지는 변경 사항에 대한 자세한 내용은 '3.x에서 4.0으로 업그레이드하기'를 참조하세요.
4.0 프리릴리스(pre-release) 사이클 동안 배포된 기능 및 수정 사항에 대한 상세한 릴리스 노트는 4.0a0, 4.0a1, 4.0rc1, 4.0rc2, 4.0rc3 및 4.0rc4에서 확인할 수 있습니다.
업그레이드 가이드는 전적으로 Claude Fable 5, Claude Opus 4.8 및 GPT-5.5가 작성했습니다. 릴리스 노트도 마찬가지입니다.
이것은 제가 로봇들에게 점차 외주를 주는 데 익숙해진 종류의 문서화 작업입니다. 이 작업은 사람들을 설득하거나 의견을 표현할 필요가 없습니다. 가능한 한 정확하고 상세하게 작성하는 것이 역할입니다. 저는 릴리스 노트를 면밀히 검토했으며, 그것들이 정확하고 포괄적임을 확인했습니다.
Claude Fable 5가 큰 도움이 되었습니다
저는 1년 전 sqlite-utils 4.0의 첫 번째 알파 버전을 출시했습니다. 메이저 버전 번호를 부여함으로써 감수하게 된 수많은 미세한 설계 결함들을 찾아내고 정리하는 데 드는 작업량 때문에 안정적인(stable) 릴리스를 계속 미뤄왔습니다.
Claude Fable 5(그리고 그보다 적은 비중으로 Opus 4.8 및 GPT-5.5)의 도움은 제가 관성을 극복하고 이 라이브러리에 할애할 수 있는 시간을 최대한 활용할 수 있도록 필요한 추진력을 제공해 주었습니다.
Fable은 API 설계에 있어 정말 뛰어난 안목을 가지고 있으며, 더 개방적인 목표를 제시하면 끊임없이 주도적으로 움직입니다. 제가 가장 성공적이었던 프롬프트는 제가 마지막 릴리스 후보(release candidate)라고 생각했던 버전에 대해 내린 리뷰 작업이었습니다:
마지막 태그된 3.x 릴리스 이후 main 브랜치에서 변경된 사항들을 리뷰해줘 - 나는 이것들을 아주 오랫동안 하위 호환성을 깨뜨리지 않는 안정적인 버전인 sqlite-utils 4.0으로 출시할 예정이야.
변경 로그(changelog)와 업그레이드 가이드를 리뷰하고, v4의 모든 새로운 기능들을 테스트해 볼 수 있는 스크립트를 직접 작성해봐 - 그 스크립트들을 저장하되 커밋(commit)하지는 마.
저는 Codex Desktop의 GPT-5.5 xhigh와 Claude Code의 Fable 5를 사용하여 이를 시도했습니다.
GPT-5.5는 5개의 Python 스크립트를 작성했지만 특별히 흥미로운 결과는 찾아내지 못했습니다. 최종 보고서는 여기에 있습니다.
Fable 5는 12개의 스크립트를 작성했고, 보고서에서 4개의 릴리스 차단 요소(release blockers)와 10개의 추가 이슈를 식별했으며, 깔끔하게 결합된 재현(repro) 스크립트를 구축했습니다. 이 스크립트를 실행했을 때 다음과 같은 결과가 출력되었습니다:
=== 1. 실패한 db.execute() 쓰기 작업이 암시적 트랜잭션(implicit transaction)을 열린 상태로 남겨둠 ===
실패한 쓰기 이후 in_transaction 상태: True
버그: 연결이 종료될 때 'other' 테이블이 조용히 손실됨
...
저는 그들이 찾아낸 거의 모든 내용에 동의하게 되었습니다. 여기 우리가 차례대로 문제를 해결해 나간 16개의 커밋이 담긴 PR(Pull Request)이 있습니다.
sqlite-utils 4.0이 최신 프론티어 모델(frontier models)의 도움 없이 만들었을 때보다 훨씬 더 높은 품질의 릴리스라는 점에는 의심의 여지가 없습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Simon Willison Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기