Simon Willison의 sqlite-utils가 v4 첫 릴리스 후보를 내며, 별도 도구였던 마이그레이션 시스템과 새 중첩 트랜잭션 API를 본체에 통합했다.

한눈에sqlite-utils 4.0rc1이 6월 21일 공개됐다 — 안정 릴리스 직전 테스트 단계그동안 별도 패키지였던 sqlite-migrate를 본체에 정식 편입SQLite의 savepoint 기능을 db.atomic() 한 줄로 쓸 수 있게 됐다

실무자: Python으로 SQLite를 다루는 작은 도구·CLI에서 스키마 변경 관리가 한층 단순해진다. 리더: 데이터 자산이 작아도 마이그레이션 같은 '어른용' 인프라 패턴을 쉽게 도입할 수 있다.

Simon Willison

이미지: Simon Willison, sqlite-utils 메인테이너, 출처: @simonw on X

sqlite-utils는 데이터 저널리스트이자 datasette 창업자인 Simon Willison이 수년째 만들어온 파이썬 라이브러리이자 명령줄 도구다. Python 표준 라이브러리에 들어 있는 sqlite3 모듈 위에 한 층을 더해, JSON 한 덩어리를 받아 자동으로 테이블을 만들어 주거나, 복잡한 테이블 변형을 한 줄로 표현하게 해 준다. 작은 데이터 분석 스크립트나 1인 프로젝트의 데이터 저장소로 SQLite를 쓰는 사람이라면 한 번쯤 거쳐 갔을 도구다.

그가 6월 21일, 메이저 버전인 v4의 첫 릴리스 후보(4.0rc1)를 공개했다. 메이저 버전 점프는 '어딘가 호환되지 않는 변경이 있다'는 신호인데, Simon은 본인 글에서 그 점을 분명히 했고 안정 버전 확정 전에 한 번 써 보고 피드백을 달라고 청했다.

무엇이 일어났나

가장 큰 변화는 두 가지다.

첫째, sqlite-migrate라는 별도 패키지로 몇 년간 따로 살아오던 데이터베이스 마이그레이션 도구가 sqlite-utils 본체로 들어왔다. 마이그레이션은 데이터베이스 스키마(테이블 모양, 컬럼 종류)를 코드로 관리하는 기법이다. 'v1에서 v2로 갈 때 이 컬럼을 추가한다'처럼 변경 사항을 함수 하나로 적어 두면, 도구가 알아서 어떤 마이그레이션이 이미 적용됐고 어떤 게 아직인지를 추적해 준다. Simon의 다른 프로젝트인 llm-cli와 여러 사용자 도구들이 이미 수년간 sqlite-migrate를 써 왔기 때문에, 설계가 충분히 검증됐다고 본 것이다.

둘째, db-atomic이라는 새 컨텍스트 매니저가 추가됐다. SQLite는 savepoint라는 기능으로 트랜잭션 안에 또 트랜잭션을 넣을 수 있는데, 이 기능을 with db.atomic(): 한 줄로 쓰게 만들었다. 이름은 Django와 Peewee에서 가져왔다고 한다.

@simonw은 트윗에서 RC 단계임을 강조하며 사용자 피드백을 요청했다.

숫자로 보기

  • 버전: 4.0rc1 — 안정 v4 릴리스 후보 첫 단계
  • Python 3.8 지원 중단, 3.13 추가
  • 마이그레이션 시스템은 이미 수년간 검증된 sqlite-migrate의 포팅

왜 중요한가

데이터베이스 마이그레이션이라고 하면 보통 Django나 Rails처럼 큰 웹 프레임워크 안에서 다루는 주제로 생각하기 쉽다. 하지만 노트 정리용 SQLite 파일, 개인 RSS 리더, 트위터 보관함 같은 1인 프로젝트에서도 '어제 만든 테이블에 오늘 컬럼을 하나 더하고 싶다'는 순간이 온다. 그때 마이그레이션 도구가 없으면 사람들이 보통 두 가지 중 하나를 한다 — (1) 매번 DB를 지우고 새로 만들거나, (2) 손으로 ALTER TABLE을 친 뒤 어딘가에 메모를 남긴다. 둘 다 위험하다.

sqlite-utils가 마이그레이션을 기본 기능으로 흡수한 건, 그런 1인 도구 작가들의 평범한 작업 흐름에 '스키마 관리'라는 작은 인프라 패턴을 자연스럽게 끼워 넣겠다는 뜻이다.

db.atomic() 쪽도 비슷한 결의 변화다. 트랜잭션 안에 트랜잭션을 넣고 일부만 되돌릴 수 있다는 건, 한 번에 여러 줄을 삽입하다가 중간에 한 줄이 실패해도 그 한 줄만 빼고 나머지를 살릴 수 있다는 뜻이다. 큰 ORM 없이 SQLite만 쓰는 코드에서도 이런 패턴을 한 줄로 표현하게 됐다.

누가 이득, 누가 손해

이득을 보는 쪽이 분명하다 — Python으로 SQLite 기반 작은 도구를 만들어 배포하는 사람들. 예전엔 sqlite-utils를 쓰면서 마이그레이션이 필요하면 sqlite-migrate를 추가 의존성으로 가져와야 했는데, 이제 한 패키지면 끝이다.

손해를 본다고까지 말할 건 아니지만, sqlite-migrate를 독립 패키지로 직접 가져다 쓰던 일부 프로젝트는 v4 전환 시점에 import 경로 정리를 한 번 해 줘야 한다.

더 깊이

Simon은 마이그레이션 시스템을 일부러 작게 유지했다고 적었다. 가장 눈에 띄는 결정은 역방향 마이그레이션을 제공하지 않는다는 것이다. Django와 Rails의 마이그레이션 시스템은 '위로(up) 적용한 변경을 되돌리는(down) 함수'를 쌍으로 적게 하는데, 실무에서 down 함수가 정확하게 동작하도록 작성하는 일이 생각보다 어렵고, 결국 거의 안 쓰이는 경우가 많다. Simon의 선택은 '실수했으면 새 마이그레이션으로 고친다'는, 단방향 forward-only 철학이다.

db.atomic() 쪽은 메인테이너 본인도 '마이그레이션보다는 검증이 덜 됐다'고 인정했다. 그래서 RC 단계의 테스트 요청에서 이 부분을 더 적극적으로 써 봐 달라고 부탁한다.

아직 알 수 없는 것

  • db.atomic()이 실전에서 어떻게 동작할지는 RC 기간의 사용자 보고에 달려 있다. Simon 본인이 '테스트가 더 필요하다'고 못 박았다.
  • 안정 v4 릴리스 일정은 명시되지 않았다.
  • 이전 with db.conn: 패턴을 쓰던 코드와 새 db.atomic() 패턴이 한 코드베이스에 섞여 있을 때의 베스트 프랙티스는 아직 사례가 없다.

5분 실습 (쉬움 · 5분)

  1. 설치 없이 한 줄로 실행: uvx --with sqlite-utils==4.0rc1 sqlite-utils --help
  2. 같은 폴더에 migrations.py를 만들고 마이그레이션 두 개를 적는다 (아래).
  3. uvx --with sqlite-utils==4.0rc1 sqlite-utils migrate notes.db migrations.py 실행.
  4. 같은 명령을 한 번 더 실행 — 이번엔 아무 일도 일어나지 않는다. 적용된 마이그레이션을 기억하기 때문이다.
  5. sqlite-utils schema notes.db로 결과 스키마를 확인한다.
from sqlite_utils import Database, Migrations
migrations = Migrations("notes")

@migrations()
def create_table(db):
    db["notes"].create({"id": int, "title": str}, pk="id")

@migrations()
def add_body(db):
    db["notes"].add_column("body", str)

더 읽어보기