cd /news/ai-agents/claude-opus-5-5-built-a-sqlite-parse… · home › topics › ai-agents › article
[ARTICLE · art-165894] src=explainx.ai ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Claude Opus 5.5 Built a SQLite parse_number() Extension: C Beat Rust on Speed

Developer Simon Willison published a November 8, 2026 walkthrough showing that Claude Opus 5.5 in regular Claude chat helped him build four loadable SQLite extensions implementing the same parse_number(value) scalar function, with the hand-written C version (c-fast) reaching about 16–19 ns/row net in published benchmarks versus roughly 329–357 ns/row for a nested SQL replace() baseline. The four builds — plain C, c-fast, rusqlite and sqlite-loadable — were validated with 40 hand tests plus 8M fuzz checks with no mismatches in the published log, and the smallest binary was the plain C build at about 14 KB stripped .so. Willison's README discloses that all text and code in the report were model-generated, and the reproducible folder is published in simonw/research.

by read9 min views2 publishedNov 8, 2026
Claude Opus 5.5 Built a SQLite parse_number() Extension: C Beat Rust on Speed
Image: Explainx (auto-discovered)

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 inClaude chat (Willison's post; not Claude Code)
What function? parse_number(value) strips, $ £ % , trims space, handles one leading- , returnsNULL on junk
How many builds? Four : plainC ,c-fast ,rusqlite ,sqlite-loadable
Fastest? c-fast at about16–19 ns/row net in published benches
Smallest binary? C at about14 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 returnsNULL when parsing fails.
  • A comparison between implementing it as a SQLite C extension versusRust loadable modules, includingperformance ,code complexity , and**.so size** .

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 andREAL inputs pass through unchanged;NULL andBLOB returnNULL .
  • Strip spaces, tabs, and newlines at both ends; remove currency symbols and commas anywhere.
  • Allow one leading minus before the first digit; allowone 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:

  1. test.py —40 curated cases checking both value and typeof() .
  2. fuzz.py — random junk and extreme-precision decimals against a Python**float()** reference;4 seeds × 500k inputs × 4 extensions withno mismatches logged in**results/fuzz.txt** .
  3. bench.py — reproduction formixed ,int , orreal 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 .so file 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.

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 acetight 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 withon-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**strtod** on slow paths; README warns** strtod** respects the host locale unless the fast path handles the input.
  • Model authorship: The research README statesLLM-generated text and code; human review still matters for security and maintenance.
  • Alpha Rust crate:****sqlite-loadable is labeledalpha in the dependency table; expect API churn.
  • Not a SQLite release: Simon's work isthird-party . Upstream SQLite docs remain the authority onextension APIs .
  • Chat versus Code: Reproducing this exact workflow needsOpus 5.5 access and manual copy-out; it is not an official Anthropic recipe.

What people building with agents should try #

  1. Scope extensions narrowly. One scalar function with a written spec beats "build me a database engine."
  2. Demand fuzz parity across language variants before picking a winner.
  3. Measure net ns/row , not wall clock on toy tables.
  4. Publish research folders so teammates diff prompts, not only binaries.
  5. 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.

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @simon willison 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/claude-opus-5-5-buil…] indexed:0 read:9min 2026-11-08 · —