메뉴
HN
Hacker News 24일 전

클로드가 작성한 sqlite-utils 4.0rc2 (약 149달러)

IMP
7/10
핵심 요약

Simon Willison은 Claude AI를 적극 활용하여 데이터베이스 트랜잭션 처리 과정의 치명적인 버그를 발견하고 수정하는 등 sqlite-utils 4.0rc2의 핵심 코드를 작성했습니다. 개발자는 휴대폰으로 AI 에이전트에게 프롬프트를 지시하는 동안 휴식을 취할 수 있었으며, 최종적으로 약 149달러의 API 비용으로 30개 파일에 달하는 대규모 코드 수정 및 설계 개선을 완료했습니다. 이는 현업 소프트웨어 개발에서 코딩 에이전트가 얼마나 강력하고 실용적인 도구가 되었는지를 보여주는 훌륭한 사례입니다.

번역된 본문

Simon Willison의 웹로그 구독 후원: Sonar — 가트너(Gartner)가 기술 부채 관리 도구 부문에서 Sonar를 2026 매직 쿼드런트™ 리더로 선정했습니다. 보고서를 읽고 코드베이스 전체의 기술 부채를 측정하고 해결하는 방법을 알아보세요. 보고서 받기

sqlite-utils 4.0rc2, 주로 Claude Fable이 작성함 (약 $149.25 소요) 2026년 7월 5일

저는 몇 주 전에 sqlite-utils 4.0rc1 릴리스에 대해 글을 썼습니다. Max 구독에서 Claude Fable을 사용할 수 있는 기간이 며칠밖에 남지 않았기 때문에, 이를 활용해 제가 정말 안심할 수 있는 안정적인 4.0 버전을 릴리스할 수 있을지 확인해보기로 했습니다. 저는 시맨틱 버저닝(SemVer)을 준수하려고 노력하며, 하위 호환성이 깨지는 메이저 버전 업데이트는 최대한 드물게 가져가는 것을 선호합니다.

저는 아이폰의 Claude Code for web에서 다음과 같은 프롬프트로 시작했습니다. "안정적인 4.0 릴리스를 배포하기 전 최종 리뷰 - 나중에 수정하면 호환성이 깨지는 breaking change가 될 수 있는 마지막 순간의 문제들을 찾는 것이 매우 중요합니다."

여기 AI가 작성한 최초의 리포트가 있습니다. 저도 아직 마주치지 못했던 심각한 문제들이 있었으며, Fable은 그중 5개를 "릴리스 차단 요소(Release blockers)"로 분류했습니다. 다음은 그중 가장 심각했던 문제입니다:

  1. delete_where()가 커밋을 하지 않고 연결을 망가뜨림 (데이터 손실) Table.delete_where() (sqlite_utils/db.py:2948)는 래핑 없이 self.db.execute()로만 DELETE를 실행하므로 연결이 in_transaction=True 상태로 남게 됩니다(Table.delete()는 올바르게 래핑하는 db.py:2944와 비교해 보세요). 결과적으로 이후의 모든 atomic() 호출이 세이브포인트 분기를 타게 되어 커밋이 전혀 이루어지지 않습니다. 재현 시나리오: db = sqlite_utils . Database ( "dw.db" ) db [ "t" ] . insert_all ([{ "id" : i } for i in range ( 3 )], pk = "id" ) db [ "t" ] . delete_where ( "id = ?" , [ 0 ]) # 이제 conn.in_transaction이 True가 됨 db [ "t" ] . insert ({ "id" : 50 }) db [ "u" ] . insert ({ "a" : 1 }) db . close ()

다시 열어보면: 행은 [0, 1, 2]입니다 — 삭제 명령, 행 50, 그리고 테이블 u가 모두 사라집니다.

이것은 정말 끔찍한 버그입니다! 이런 버그를 배포하지 않게 되어 정말 기쁩니다. 비록 이 버그가 나중에라도 4.0.1 패치 릴리스로 수정할 수 있는 버그였을지언정, 5.0으로 가야만 하는 설계 결함은 아니었을 것입니다.

총 37개의 프롬프트, 34번의 커밋, 30개의 개별 파일에 걸친 +1,321 / -190 라인의 코드 변경을 거치면서, 우리는 피드백 전체를 차례대로 해결했으며 과정 중에 몇 가지 다른 설계 개선사항도 함께 적용했습니다.

코딩 에이전트의 이상한 점은 에이전트가 새 작업을 처리하는 데 가끔 10~15분 정도 소요되기 때문에, 이런 까다로운 작업일수록 오히려 다른 일을 할 기회가 더 많아진다는 것입니다. 저는 하프문 베이(Half Moon Bay) 7월 4일 퍼레이드를 즐기러 나가서, 가끔씩 확인하며 휴대폰으로 Fable에게 다음 단계를 지시했습니다. 자세한 내용은 PR(Pull Request)과 이 공유된 대화 기록에서 확인할 수 있습니다.

저는 최종 리뷰를 위해 노트북으로 전환했으며, GitHub의 PR 인터페이스를 통해 리뷰를 진행했습니다. 가장 중요한 변경 사항은 트랜잭션 처리와 관련된 것으로, 이는 이전 RC 버전의 핵심적인 새 기능이었습니다. 새로운 RC 버전에는 새로운 트랜잭션 모델에 대한 포괄적인 문서가 포함되어 있으며, 그 서론을 여기에 전체 인용하겠습니다:

데이터베이스에 쓰기를 수행하는 이 라이브러리의 모든 메서드(insert(), upsert(), update(), delete(), delete_where(), transform(), create_table(), create_index(), enable_fts() 등)는 자체 트랜잭션 내부에서 실행되며, 반환되기 전에 커밋됩니다. 즉, 메서드 호출이 완료되는 즉시 변경 사항이 디스크에 저장됩니다: db = Database ( "data.db" ) db . table ( "news" ). insert ({ "headline" : "Dog wins award" })

새 행이 이미 저장됨 - commit() 필요 없음

db.execute()로 실행되는 원시 SQL에도 동일하게 적용됩니다 — 쓰기 명령문은 실행되자마자 커밋됩니다. 사용자는 결코 commit()을 호출할 필요가 없으며, 변경 사항을 저장하기 위해 데이터베이스를 닫을 필요도 없습니다. 트랜잭션에 대해 고려해야 하는 정확히 두 가지 상황은 다음과 같습니다:

  1. 여러 쓰기 작업을 그룹화하여 모두 성공하거나 모두 실패하게 만들려면 db.atomic()을 사용하세요.
  2. db.begin()으로 트랜잭션을 직접 관리하는 경우, 이때는 사용자가 커밋(또는 롤백)을 실행하기 전까지는 어떠한 내용도 커밋되지 않습니다.
원문 보기
원문 보기 (영어)
Simon Willison’s Weblog Subscribe Sponsored by: Sonar — Gartner just named Sonar a Leader in the 2026 Magic Quadrant™ for Technical Debt Management Tools. Read the report and learn how to measure and remediate technical debt across your codebase. Get the report sqlite-utils 4.0rc2, mostly written by Claude Fable (for about $149.25) 5th July 2026 I wrote about the sqlite-utils 4.0rc1 release a couple of weeks ago. Since we only have Claude Fable on our Max subscriptions for a few more days, I decided to see if it could help me get to a 4.0 stable release that I felt truly comfortable about, since I try to keep to SemVer and like my incompatible major versions to be as rare as possible. I started with this prompt, in Claude Code for web on my iPhone: Final review before shipping a stable 4.0 release - very important to spot any last minute things that would be a breaking change if we fix them later Here’s that initial report it created for me. There were some significant problems that I hadn’t myself encountered yet—5 that Fable categorized as “release blockers”. Here’s the worst of the bunch: 1. delete_where() never commits and poisons the connection (data loss) Table.delete_where() ( sqlite_utils/db.py:2948 ) runs its DELETE via a bare self.db.execute() with no atomic() wrapper — compare Table.delete() at db.py:2944 , which wraps correctly. The connection is left in_transaction=True , so every subsequent atomic() call takes the savepoint branch ( db.py:430-440 ) and never commits either. Reproduced end-to-end: db = sqlite_utils . Database ( "dw.db" ) db [ "t" ]. insert_all ([{ "id" : i } for i in range ( 3 )], pk = "id" ) db [ "t" ]. delete_where ( "id = ?" , [ 0 ]) # conn.in_transaction is now True db [ "t" ]. insert ({ "id" : 50 }) db [ "u" ]. insert ({ "a" : 1 }) db . close () # Reopen: rows are [0, 1, 2] — the delete, row 50, AND table u are all gone. That’s a really bad bug! Very glad I didn’t ship that, although at least it would have been a bug I could fix in a 4.0.1 point release, not a design flaw that would force a 5.0. Over the course of 37 prompts, 34 commits and +1,321 -190 code changes over 30 separate files, we worked through the entire set of feedback in turn, making several other design improvements along the way. A weird thing about coding agents is that harder tasks like this one actually provide more opportunity to do other things at the same time, since the agent sometimes needs 10-15 minutes to churn away on a new task. I went out to enjoy the Half Moon Bay 4th of July parade, occasionally checking in and prompting the next step for Fable from my phone. Full details in the PR and this shared transcript . I switched to my laptop for the final review, which I conducted through GitHub’s PR interface. The most significant changes relate to transaction handling, which was the signature new feature in the earlier RC . The new RC now includes comprehensive documentation on the new transaction model, the intro to which I’ll quote here in full: Every method in this library that writes to the database— insert() , upsert() , update() , delete() , delete_where() , transform() , create_table() , create_index() , enable_fts() and the rest—runs inside its own transaction and commits it before returning. Your changes are saved to disk as soon as the method call finishes: db = Database ( "data.db" ) db . table ( "news" ). insert ({ "headline" : "Dog wins award" }) # The new row is already saved - no commit() required The same applies to raw SQL executed with db.execute() —a write statement is committed as soon as it has run. You never need to call commit() , and you do not need to close the database to persist your changes. There are exactly two situations where you need to think about transactions: You want to group several write operations together, so they either all succeed or all fail—use db.atomic() . You are managing a transaction yourself with db.begin() , in which case nothing is committed until you commit—the library will never commit a transaction you opened. In reviewing Fable’s documentation—I find that reviewing the documentation edits first is an excellent way to build an initial understanding of what has changed—I spotted this detail : db.atomic() and the automatic per-method transactions are designed for connections in Python’s default transaction handling mode. Connections created with the Python 3.12+ sqlite3.connect(..., autocommit=True) or autocommit=False options are not supported, because commit() and rollback() behave differently on those connections. I admit I hadn’t thought about how sqlite-utils would react to the more recent autocommit setting , added in Python 3.12. It turns out “behave differently on those connections” equated to almost the entire test suite failing, so I worked with the model to ensure that this difference would not break how the library works. And a final review by GPT-5.5 I used to think that the idea of having one model review the work of another was somewhat absurd—it felt weirdly superstitious. The problem is it really does work —I’ve started habitually having Anthropic’s best model review OpenAI’s work and vice versa, because I’ve had that turn up interesting results often enough to be valuable. I prompted Codex Desktop and GPT-5.5 xhigh with the following: Review changes since the last RC. Also confirm that the changelog is up-to-date. Which was enough to turn up two issues worth investigating: Findings [P1] sqlite_utils/db.py:663 db.query() now rejects non-row statements only after calling db.execute() , and sqlite_utils/db.py:705 auto-commits those writes first. So db.query("update ...") raises ValueError but the update is already committed. That is a surprising side effect for a method documented as “can only be used with SQL that returns rows.” [P1] sqlite_utils/db.py:672 INSERT ... RETURNING through db.query() only commits after the returned generator is fully exhausted. db.query("insert ... returning ...") without iteration, or common next(db.query(...)) usage, leaves the transaction open and the write can be rolled back on close. This contradicts docs/changelog.rst:15 and docs/python-api.rst:232 , which say it takes effect without iteration. I pasted that into a fresh Fable session, which ran some experiments to confirm the problem: Both findings were confirmed. db.query() called self.execute() first, which auto-commits writes, and only then checked cursor.description — so db.query("update ...") committed the update before raising ValueError . And the INSERT ... RETURNING commit lived at the end of the returned generator, so it never fired unless you exhausted the iterator — next(db.query(...)) or an un-iterated call left the transaction open, contradicting what the changelog and docs promise. Here’s the PR with the fix, and the full Claude Code transcript . Reviewing this code helped me build a better mental model of the edge cases of SQLite transaction semantics! For an estimated (unsubsidized) cost of $149.25 I upgraded to the Claude Max $200/month plan (I was previously on $100/month) to increase my Fable allowance for the remaining time until the July 7th Fablepocalypse , when even Claude Max subscribers will have to pay full API cost for the model. I was curious as to how much this would have cost me if I had been paying those costs directly. At first I thought those numbers weren’t available to me since I had run the work remotely using Claude Code for web, and then I realized I could run AgentsView inside that existing session to get that cost estimate! Run "uvx agentsview --help"​ and then use that tool to calculate the cost of this session Claude figured out how to use the session list --include-children command and came out with the following: Transcript Model Cost Main session claude-fable-5 $141.02 API-surface sweep agent claude-fable-5 $2.40 Transactions/atomic review agent claude-fable-5 $2.39 Post-rc1 commits review agent claude-fable-5 $1.72 Migrations review