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

> Source: <https://explainx.ai/blog/claude-opus-5-5-sqlite-parse-number-c-vs-rust-simon-willison-research-2026?utm_source=rss&utm_medium=feed&utm_campaign=blog>
> Published: 2026-11-08 00:00:00+00:00

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](https://github.com/simonw/research/tree/main/sqlite-parse-number-c-vs-rust?ref=explainx)**. 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](https://github.com/simonw/research/tree/main/sqlite-parse-number-c-vs-rust?ref=explainx) | 

## What Willison asked Opus 5.5 to do

In [his November 8 post](https://simonwillison.net/2026/Nov/8/sqlite-parse-number-c-vs-rust/?ref=explainx), 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**`.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](https://github.com/simonw/research/tree/main/sqlite-parse-number-c-vs-rust?ref=explainx) 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:

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** with**no mismatches** logged in**`results/fuzz.txt`** .
3. **`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](https://explainx.ai/blog/llm-0-37-simon-willison-gpt-6-1-sol-terra-openai-compatible-endpoint-add-guide-2026)**, 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](https://explainx.ai/blog/battle-drones-simon-willison-vibe-coded-rts-audio-multiplayer-claude-code-321-commits-2026), 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.

## 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](https://explainx.ai/blog/semianalysis-gdpx-benchmark-opus-5-5-12-percent-agents-fail-2026) show frontier models still fail most long-horizon tasks even while they ace**tight numeric coding loops** like this one.
- [Weight-space editing experiments](https://explainx.ai/blog/weight-space-programming-gpt-6-astra-edits-qwen3-5-0-8b-weights-math-7-9-to-48-1-mit-csail-2026) quote Willison's earlier work interpreting weights locally, a different axis of "model meets low-level code."
- Our **[MLX Apple Silicon guide](https://explainx.ai/blog/mlx-apple-silicon-run-llms-complete-guide-2026)** 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](https://explainx.ai/blog/agent-sandboxes-coding-agents-compared-e2b-modal-daytona-guide-2026)** 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 states**LLM-generated** text and code; human review still matters for security and maintenance.
- **Alpha Rust crate:****`sqlite-loadable`** is 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

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](https://explainx.ai/blog/vector-databases-rag-compared-pgvector-qdrant-chroma-weaviate-2026) when you outgrow single-file analytics.

## Related reading

*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.*
