On November 8, 2026, developer Simon Willison published a walkthrough of how regular Claude chat with Opus 5.5 helped him build four loadable SQLite extensions that implement the same parse_number(value) scalar function, benchmark them against pure SQL, and ship a reproducible folder in simonw/research. The hook is practical: turn messy spreadsheet strings such as -$1,234.56 or 12.5% into numbers you can SUM and ORDER BY inside SQLite.
This is not a product launch from Anthropic or SQLite. It is a documented agent-assisted systems programming session with numbers readers can re-run. If you build analytics pipelines, local-first apps, or agent tools that ingest CSV exports, the lesson is how far a well-scoped native extension prompt can get before you touch a full coding agent repo.
TL;DR
| Question | Answer |
|---|---|
| Which models? | Claude Opus 5.5 in Claude chat (Willison's post; not Claude Code) |
| What function? | parse_number(value) strips , $ £ %, trims space, handles one leading -, returns NULL on junk |
| How many builds? | Four: plain C, c-fast, rusqlite, sqlite-loadable |
| Fastest? | c-fast at about 16–19 ns/row net in published benches |
| Smallest binary? | C at about 14 KB stripped .so |
| Slowest baseline? | Nested SQL replace() at ~329–357 ns/row |
| Correctness? | 40 hand tests + 8M fuzz checks, no mismatches in published log |
| Where is source? | GitHub research folder |
What Willison asked Opus 5.5 to do
In his November 8 post, Willison says the idea started on a dog walk. He opened Claude and prompted for:
- A
parse_number(value)custom function that strips$ £ % ,, tolerates whitespace, allows one leading minus, and returns NULL when parsing fails. - A comparison between implementing it as a SQLite C extension versus Rust loadable modules, including performance, code complexity, and
.sosize.
The first pass surprised him: Rust binaries were hundreds of kilobytes but faster than plain C on mixed data because strtod in glibc lost on decimals. He followed up:
Try writing your own C function for parsing text into a decimal number that catches up to or beats the Rust implementation
That second prompt produced c-fast/, the eventual winner on the benchmark table.
He then asked Claude to clone the simonw/research layout and deliver a zip-style research package. The published README begins with a disclosure that all text and code in that report were model-generated, pointing readers to the parent repo's explanation of AI-authored research notes.
Behavior rules the model chose
Because the initial prompt left edge cases open, Opus 5.5 fixed a shared ruleset used by all four builds:
- INTEGER and REAL inputs pass through unchanged; NULL and BLOB return NULL.
- Strip spaces, tabs, and newlines at both ends; remove currency symbols and commas anywhere.
- Allow one leading minus before the first digit; allow one decimal point with at least one digit.
- Reject scientific notation, interior spaces, doubled minus signs, and ambiguous forms like
5-.
Example calls from the README:
select parse_number(' -$1,234.56 '); -- -1234.56
select parse_number(' £1,000,000 '); -- 1000000 (INTEGER)
select parse_number(' 12.5% '); -- 12.5
select parse_number(' n/a '); -- NULL
Those rules matter for agents. If you let a model invent parsing ad hoc in SQL, you get inconsistent dashboards. Centralizing in a scalar function gives agents a single predicate to reference in generated queries.
Benchmark numbers from the research README
The repo runs select sum(parse_number(v)) over two million rows inside Python's sqlite3 module (SQLite 3.53.1). It subtracts a baseline scan to report net nanoseconds per row. Hardware was a 2 vCPU Linux x86-64 VM with gcc 13.3 and rustc 1.97.
| Dataset | plain C | fast C | rusqlite | sqlite-loadable | pure SQL replace() |
|---|---|---|---|---|---|
| Mixed currency strings | 67.3 | 19.2 | 45.9 | 40.5 | 328.9 |
| Integers only | 25.1 | 15.8 | 36.4 | 32.1 | 342.5 |
| Decimals only | 106.3 | 17.5 | 54.5 | 46.5 | 356.9 |
Willison's blog post rounds those to 16–19 ns for c-fast across suites. The takeaway for builders is not that C always wins. It is that library float parsing choices (strtod versus Rust's f64::from_str) dominated until the model rewrote a Clinger-style fast path inside the scan loop.
The README documents why plain C lost on decimals and how c-fast detects when mantissa and fraction lengths fit exact IEEE operations, falling back to strtod only on long inputs.
Size and complexity trade-offs
| Variant | Stripped .so | Lines of core code (per README) | Build time from scratch |
|---|---|---|---|
| plain C | 14,408 B | 75 | ~0.1 s |
| c-fast | 14,408 B | 104 | ~0.1 s |
| rusqlite | 320,864 B | 36 + 41 shared parser | ~11 s |
| sqlite-loadable | 298,464 B | 30 + 41 shared parser | ~72 s (bindgen) |
Rust builds drag dozens of crates even for one function. For desktop analytics that is fine. For shipping a single .so inside a mobile app or SQLite shell plugin, the 14 KB C artifact is the headline.
Willison also notes Rust's ergonomics: overflow-safe integer parsing, ValueRef matches, and less manual buffer bookkeeping. The README's "Notes on writing each one" section is essentially a micro trade study you can hand to a junior engineer before they pick a language for the next extension.
The README's micro-benchmark notes explain why rusqlite stayed slower than c-fast even after parser tweaks: each call pays for closure wrapping, Result handling, and a per-row Vec allocation. Swapping that Vec for a stack buffer only shaved a few nanoseconds on integer-only data. If your workload is mostly integers and you already ship Rust, the sqlite-loadable or rusqlite path may be good enough. When decimals dominate and binary size matters, c-fast/ is the documented winner on this VM.
Federated spools linking vessels, metaphor for SQLite parse_number feeding many analytic queries
Image: Library illustration for data parsing pipelines; not from the research repo.
Correctness checks you can rerun
The repo ships three scripts:
test.py— 40 curated cases checking both value andtypeof().fuzz.py— random junk and extreme-precision decimals against a Pythonfloat()reference; 4 seeds × 500k inputs × 4 extensions with no mismatches logged inresults/fuzz.txt.bench.py— reproduction for mixed, int, or real suites.
Reproduction commands from the README:
./build.sh
python test.py
python fuzz.py 1
python bench.py mixed
Or compile only the fast C path:
gcc -O2 -fPIC -shared c-fast/parse_number.c -o parse_number.so
For readers already using Willison's LLM CLI 0.37, this repo is the same author showing chat models doing native code iteration outside that tool's Python packaging.
How this compares to agent harness workflows
Willison did not open a repository in Claude Code or Codex. He iterated in chat, then asked the model to format results like his research series. That is closer to how product managers prototype SQL UDFs than to how teams run hooked agent loops on monorepos.
Contrast with Battle Drones, where nearly all commits came from Claude Code across hundreds of iterations. The SQLite story shows narrow mechanical prompts still excel when:
- The acceptance tests are numerical and fuzzable.
- The deliverable is a self-contained directory, not a production service.
- You can manually spot-check one
.sofile before merge.
For teams standardized on coding agents, the migration path is straightforward: drop the research folder into a repo, wire build.sh into CI, and let agents extend parse_number for locale-specific symbols under the same fuzz harness.
Links to wider Claude and local-AI coverage
This sits beside other November posts on Opus 5.5 capability and limits:
- GDPx agentic benchmark scores show frontier models still fail most long-horizon tasks even while they ace tight numeric coding loops like this one.
- Weight-space editing experiments quote Willison's earlier work interpreting weights locally, a different axis of "model meets low-level code."
- Our MLX Apple Silicon guide covers local inference stacks; SQLite extensions pair naturally with on-device analytics when you do not want a server between CSV and chart.
If you embed SQLite inside agent tools, remember loadable extensions are native code. Review them like any supply-chain binary. Sandboxing patterns from our agent sandbox comparison still apply when an agent can LOAD EXTENSION from an untrusted path.
Red-team report folder for validating SQLite parse_number extension fuzz tests
Image: Library illustration for correctness testing discipline.
Limits and caveats
- Locale: Both C paths ultimately depend on
strtodon slow paths; README warnsstrtodrespects the host locale unless the fast path handles the input. - Model authorship: The research README states LLM-generated text and code; human review still matters for security and maintenance.
- Alpha Rust crate:
sqlite-loadableis labeled alpha in the dependency table; expect API churn. - Not a SQLite release: Simon's work is third-party. Upstream SQLite docs remain the authority on extension APIs.
- Chat versus Code: Reproducing this exact workflow needs Opus 5.5 access and manual copy-out; it is not an official Anthropic recipe.
What people building with agents should try
- Scope extensions narrowly. One scalar function with a written spec beats "build me a database engine."
- Demand fuzz parity across language variants before picking a winner.
- Measure net ns/row, not wall clock on toy tables.
- Publish research folders so teammates diff prompts, not only binaries.
- Promote winning code to CI once chat iteration stabilizes.
Packaging for teammates who do not use Claude
If you want others to review the work without an Anthropic login, mirror the simonw/research folder into your org git, pin the bench.py seed, and attach results/fuzz.txt to the pull request. Reviewers then argue about strtod fast paths and rusqlite closure overhead, not about chat transcripts. That habit transfers directly to agent pipelines that emit SQL against customer exports: the extension becomes a versioned artifact, while the prompt stays disposable. For embedding-heavy stacks that also load vectors beside SQLite, see how pgvector and peers compare when you outgrow single-file analytics.
Related reading
- Simon Willison's November 8 announcement
- Full GitHub research tree
- LLM 0.37 CLI release from the same author
- Battle Drones and Claude Code iteration count
- GDPx and Opus 5.5 agentic scores
- MLX local model guide for Apple Silicon
- Agent sandboxes for untrusted code paths
- Zvec and in-process data engines
Benchmark figures and file sizes are taken from the simonw/research README as of November 8, 2026. Claude model availability and chat features may differ by account region and plan.
