{"slug": "claude-opus-5-5-built-a-sqlite-parse-number-extension-c-beat-rust-on-speed", "title": "Claude Opus 5.5 Built a SQLite parse_number() Extension: C Beat Rust on Speed", "summary": "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.", "body_md": "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.\n\nThis 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.\n\n## TL;DR\n\n| Question | Answer | \n|---|---|\n| Which models? | **Claude Opus 5.5** in**Claude chat** (Willison's post; not Claude Code) | \n| What function? | **`parse_number(value)`** strips`, $ £ %` , trims space, handles one leading`-` , returns**NULL** on junk | \n| How many builds? | **Four** : plain**C** ,**c-fast** ,**rusqlite** ,**sqlite-loadable** | \n| Fastest? | **c-fast** at about**16–19 ns/row** net in published benches | \n| Smallest binary? | **C** at about**14 KB** stripped**.so** | \n| Slowest baseline? | Nested SQL **`replace()`** at**~329–357 ns/row** | \n| Correctness? | **40** hand tests +**8M** fuzz checks, no mismatches in published log | \n| Where is source? | [GitHub research folder](https://github.com/simonw/research/tree/main/sqlite-parse-number-c-vs-rust?ref=explainx) | \n\n## What Willison asked Opus 5.5 to do\n\nIn [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:\n\n- A **`parse_number(value)`** custom function that strips**`$ £ % ,`** , tolerates whitespace, allows one leading minus, and returns**NULL** when parsing fails.\n- A comparison between implementing it as a **SQLite C extension** versus**Rust** loadable modules, including**performance** ,**code complexity** , and**`.so` size** .\n\nThe 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:\n\nTry writing your own C function for parsing text into a decimal number that catches up to or beats the Rust implementation\n\nThat second prompt produced **`c-fast/`**, the eventual winner on the benchmark table.\n\nHe 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.\n\n## Behavior rules the model chose\n\nBecause the initial prompt left edge cases open, Opus 5.5 fixed a shared ruleset used by all four builds:\n\n- **INTEGER** and**REAL** inputs pass through unchanged;**NULL** and**BLOB** return**NULL** .\n- Strip spaces, tabs, and newlines at both ends; remove currency symbols and commas anywhere.\n- Allow **one** leading minus before the first digit; allow**one** decimal point with at least one digit.\n- Reject scientific notation, interior spaces, doubled minus signs, and ambiguous forms like **`5-`** .\n\nExample calls from the README:\n\n```\nselect parse_number(' -$1,234.56 '); -- -1234.56\nselect parse_number(' £1,000,000 '); -- 1000000 (INTEGER)\nselect parse_number(' 12.5% '); -- 12.5\nselect parse_number(' n/a '); -- NULL\n```\n\nThose 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.\n\n## Benchmark numbers from the research README\n\nThe 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**.\n\n| Dataset | plain C | fast C | rusqlite | sqlite-loadable | pure SQL `replace()` | \n|---|---|---|---|---|---|\n| Mixed currency strings | 67.3 | **19.2** | 45.9 | 40.5 | 328.9 | \n| Integers only | 25.1 | **15.8** | 36.4 | 32.1 | 342.5 | \n| Decimals only | 106.3 | **17.5** | 54.5 | 46.5 | 356.9 | \n\nWillison'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.\n\nThe 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.\n\n## Size and complexity trade-offs\n\n| Variant | Stripped `.so` | Lines of core code (per README) | Build time from scratch | \n|---|---|---|---|\n| plain C | 14,408 B | 75 | ~0.1 s | \n| c-fast | 14,408 B | 104 | ~0.1 s | \n| rusqlite | 320,864 B | 36 + 41 shared parser | ~11 s | \n| sqlite-loadable | 298,464 B | 30 + 41 shared parser | ~72 s (bindgen) | \n\nRust 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.\n\nWillison 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.\n\nThe 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.\n\nFederated spools linking vessels, metaphor for SQLite parse_number feeding many analytic queries\n\n*Image: Library illustration for data parsing pipelines; not from the research repo.*\n\n## Correctness checks you can rerun\n\nThe repo ships three scripts:\n\n1. **`test.py`** —**40** curated cases checking both value and` typeof()` .\n2. **`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`** .\n3. **`bench.py`** — reproduction for**mixed** ,**int** , or**real** suites.\n\nReproduction commands from the README:\n\n```\n./build.sh\npython test.py\npython fuzz.py 1\npython bench.py mixed\n```\n\nOr compile only the fast C path:\n\n```\ngcc -O2 -fPIC -shared c-fast/parse_number.c -o parse_number.so\n```\n\nFor 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.\n\n## How this compares to agent harness workflows\n\nWillison 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.\n\nContrast 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:\n\n- The acceptance tests are **numerical and fuzzable** .\n- The deliverable is a **self-contained directory** , not a production service.\n- You can manually spot-check **one `.so` file** before merge.\n\nFor 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.\n\n## Links to wider Claude and local-AI coverage\n\nThis sits beside other November posts on **Opus 5.5** capability and limits:\n\n- [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.\n- [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.\"\n- 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.\n\nIf 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.\n\nRed-team report folder for validating SQLite parse_number extension fuzz tests\n\n*Image: Library illustration for correctness testing discipline.*\n\n## Limits and caveats\n\n- **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.\n- **Model authorship:** The research README states**LLM-generated** text and code; human review still matters for security and maintenance.\n- **Alpha Rust crate:****`sqlite-loadable`** is labeled**alpha** in the dependency table; expect API churn.\n- **Not a SQLite release:** Simon's work is**third-party** . Upstream SQLite docs remain the authority on**extension APIs** .\n- **Chat versus Code:** Reproducing this exact workflow needs**Opus 5.5** access and manual copy-out; it is not an official Anthropic recipe.\n\n## What people building with agents should try\n\n1. **Scope extensions narrowly.** One scalar function with a written spec beats \"build me a database engine.\"\n2. **Demand fuzz parity** across language variants before picking a winner.\n3. **Measure net ns/row** , not wall clock on toy tables.\n4. **Publish research folders** so teammates diff prompts, not only binaries.\n5. **Promote winning code to CI** once chat iteration stabilizes.\n\n### Packaging for teammates who do not use Claude\n\nIf 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.\n\n## Related reading\n\n*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.*", "url": "https://wpnews.pro/news/claude-opus-5-5-built-a-sqlite-parse-number-extension-c-beat-rust-on-speed", "canonical_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_at": "2026-11-08 00:00:00+00:00", "updated_at": "2026-11-09 02:01:47.164973+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Simon Willison", "Claude Opus 5.5", "Anthropic", "SQLite", "simonw/research", "rusqlite", "sqlite-loadable"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-opus-5-5-built-a-sqlite-parse-number-extension-c-beat-rust-on-speed", "markdown": "https://wpnews.pro/news/claude-opus-5-5-built-a-sqlite-parse-number-extension-c-beat-rust-on-speed.md", "text": "https://wpnews.pro/news/claude-opus-5-5-built-a-sqlite-parse-number-extension-c-beat-rust-on-speed.txt", "jsonld": "https://wpnews.pro/news/claude-opus-5-5-built-a-sqlite-parse-number-extension-c-beat-rust-on-speed.jsonld"}}