<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet href="/feed.css?v=208f42b55cff" type="text/css"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:site="https://lalitm.com/feed/ns#"><channel><title>Lalit Maganti (Tag: Claude-Code)</title><link>https://lalitm.com/tags/claude-code/</link><description>Recent content tagged Claude-Code on Lalit Maganti</description><site:notice>This is a feed.
Feeds let you subscribe to updates from this site using a feed reader. Copy this page's URL from your address bar and paste it into your reader.
New to feeds? Read: https://aboutfeeds.com</site:notice><docs>https://aboutfeeds.com</docs><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 05 Apr 2026 13:00:00 +0100</lastBuildDate><atom:link href="https://lalitm.com/tags/claude-code/index.xml" rel="self" type="application/rss+xml"/><item><title>Eight years of wanting, three months of building with AI</title><link>https://lalitm.com/post/building-syntaqlite-ai/</link><pubDate>Sun, 05 Apr 2026 13:00:00 +0100</pubDate><guid>https://lalitm.com/post/building-syntaqlite-ai/</guid><description>For eight years, I’ve wanted a high-quality set of devtools for working with SQLite. Given how important SQLite is to the industry1, I’ve long been puzzled that no one has invested in building a really good developer experience for it2.
A couple of weeks ago, after ~250 hours of effort over three months3 on evenings, weekends, and vacation days, I finally released syntaqlite (GitHub), fulfilling this long-held wish. And I believe the main reason this happened was because of AI coding agents4.
Of course, there’s no shortage of posts claiming that AI one-shot their project or pushing back and declaring that AI is all slop. I’m going to take a very different approach and, instead, systematically break down my experience building syntaqlite with AI, both where it helped and where it was detrimental.
I’ll do this while contextualizing the project and my background so you can independently assess how generalizable this experience was. And whenever I make a claim, I’ll try to back it up with evidence from my project journal, coding transcripts, or commit history5.</description><content:encoded>&lt;p&gt;For eight years, I&amp;rsquo;ve wanted a high-quality set of devtools for working with
SQLite. Given how important SQLite is to the industry&lt;sup class="sn-ref" id="sn-ref-sqlite-industry"&gt;&lt;a href="#sn-sqlite-industry"&gt;1&lt;/a&gt;&lt;/sup&gt;, I&amp;rsquo;ve long been puzzled that no one has invested in building
a really good developer experience for it&lt;sup class="sn-ref" id="sn-ref-devtools"&gt;&lt;a href="#sn-devtools"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;A couple of weeks ago, after ~250 hours of effort over three months&lt;sup class="sn-ref" id="sn-ref-hours"&gt;&lt;a href="#sn-hours"&gt;3&lt;/a&gt;&lt;/sup&gt; on evenings, weekends, and vacation days, I finally
&lt;a href="https://lalitm.com/post/syntaqlite/"&gt;released syntaqlite&lt;/a&gt;
(&lt;a href="https://github.com/LalitMaganti/syntaqlite"&gt;GitHub&lt;/a&gt;), fulfilling this
long-held wish. And I believe the main reason this happened was because of AI
coding agents&lt;sup class="sn-ref" id="sn-ref-codingtools"&gt;&lt;a href="#sn-codingtools"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Of course, there&amp;rsquo;s no shortage of posts claiming that AI one-shot their project
or pushing back and declaring that AI is all slop. I&amp;rsquo;m going to take a very
different approach and, instead, systematically break down my experience
building syntaqlite with AI, both where it helped &lt;em&gt;and&lt;/em&gt; where it was
detrimental.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll do this while contextualizing the project and my background so you can
independently assess how generalizable this experience was. And whenever I make
a claim, I&amp;rsquo;ll try to back it up with evidence from my project journal, coding
transcripts, or commit history&lt;sup class="sn-ref" id="sn-ref-evidence"&gt;&lt;a href="#sn-evidence"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id="why-i-wanted-it"&gt;Why I wanted it&lt;a class="heading-anchor" href="#why-i-wanted-it" aria-label="Permalink to Why I wanted it"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;In my work on &lt;a href="https://docs.perfetto.dev"&gt;Perfetto&lt;/a&gt;, I maintain a SQLite-based
language for querying performance traces called
&lt;a href="https://perfetto.dev/docs/analysis/perfetto-sql-getting-started"&gt;PerfettoSQL&lt;/a&gt;.
It&amp;rsquo;s basically the same as SQLite but with a few extensions to make the trace
querying experience better. There are ~100K lines of PerfettoSQL internally in
Google and it&amp;rsquo;s used by a wide range of teams.&lt;/p&gt;
&lt;p&gt;Having a language which gets traction means your users also start expecting
things like formatters, linters, and editor extensions. I&amp;rsquo;d hoped that we could
adapt some SQLite tools from open source but the more I looked into it, the more
disappointed I was. What I found either wasn&amp;rsquo;t reliable enough, fast
enough&lt;sup class="sn-ref" id="sn-ref-speed-comparison"&gt;&lt;a href="#sn-speed-comparison"&gt;6&lt;/a&gt;&lt;/sup&gt;, or flexible enough to adapt to PerfettoSQL. There was
clearly an opportunity to build something from scratch, but it was never the
&amp;ldquo;most important thing we could work on&amp;rdquo;. We&amp;rsquo;ve been reluctantly making do with
the tools out there but always wishing for better.&lt;/p&gt;
&lt;p&gt;On the other hand, there &lt;em&gt;was&lt;/em&gt; the option to do something in my spare time. I
had built lots of open source projects in my teens&lt;sup class="sn-ref" id="sn-ref-holoirc"&gt;&lt;a href="#sn-holoirc"&gt;7&lt;/a&gt;&lt;/sup&gt; but this
had faded away during university when I felt that I just didn&amp;rsquo;t have the
motivation anymore. Being a maintainer is much more than just &amp;ldquo;throwing the code
out there&amp;rdquo; and seeing what happens. It&amp;rsquo;s triaging bugs, investigating crashes,
writing documentation, building a community, and, most importantly, having a
direction for the project.&lt;/p&gt;
&lt;p&gt;But the itch of open source (specifically freedom to work on what I wanted while
helping others) had never gone away. The SQLite devtools project was eternally
in my mind as &amp;ldquo;something I&amp;rsquo;d like to work on&amp;rdquo;. But there was another reason why
I kept putting it off: it sits at the intersection of being both hard &lt;em&gt;and&lt;/em&gt;
tedious.&lt;/p&gt;
&lt;h2 id="what-makes-it-hard-and-tedious"&gt;What makes it hard and tedious&lt;a class="heading-anchor" href="#what-makes-it-hard-and-tedious" aria-label="Permalink to What makes it hard and tedious"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;If I was going to invest my personal time working on this project, I didn&amp;rsquo;t want
to build something that only helped Perfetto: I wanted to make it work for &lt;em&gt;any&lt;/em&gt;
SQLite user out there&lt;sup class="sn-ref" id="sn-ref-ambition"&gt;&lt;a href="#sn-ambition"&gt;8&lt;/a&gt;&lt;/sup&gt;. And this means parsing SQL &lt;em&gt;exactly&lt;/em&gt;
like SQLite.&lt;/p&gt;
&lt;p&gt;The heart of any language-oriented devtool is the parser. This is responsible
for turning the source code into a &amp;ldquo;parse tree&amp;rdquo; which acts as the central data
structure anything else is built on top of. If your parser isn&amp;rsquo;t accurate, then
your formatters and linters will inevitably inherit those inaccuracies; many of
the tools I found suffered from having parsers which approximated the SQLite
language rather than representing it precisely.&lt;/p&gt;
&lt;p&gt;Unfortunately, unlike many other languages, SQLite has no formal specification
describing how it should be parsed. It doesn&amp;rsquo;t expose a stable API for its
parser either. In fact, quite uniquely, in its implementation it doesn&amp;rsquo;t even
build a parse tree at all&lt;sup class="sn-ref" id="sn-ref-no-parse-tree"&gt;&lt;a href="#sn-no-parse-tree"&gt;9&lt;/a&gt;&lt;/sup&gt;! The only reasonable approach
left in my opinion is to carefully extract the relevant parts of SQLite&amp;rsquo;s source
code and adapt it to build the parser I wanted&lt;sup class="sn-ref" id="sn-ref-extraction"&gt;&lt;a href="#sn-extraction"&gt;10&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;This means getting into the weeds of SQLite source code, a fiendishly difficult
codebase to understand. The whole project is written in C in an
&lt;a href="https://sqlite.org/src/file?name=src/vdbe.c&amp;amp;ci=trunk"&gt;incredibly dense style&lt;/a&gt;;
I&amp;rsquo;ve spent days just understanding the virtual table
&lt;a href="https://www.sqlite.org/vtab.html"&gt;API&lt;/a&gt;&lt;sup class="sn-ref" id="sn-ref-vtab-nuance"&gt;&lt;a href="#sn-vtab-nuance"&gt;11&lt;/a&gt;&lt;/sup&gt; and
&lt;a href="https://sqlite.org/src/file?name=src/vtab.c&amp;amp;ci=trunk"&gt;implementation&lt;/a&gt;. Trying
to grasp the full parser stack was daunting.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the fact that there are &amp;gt;400 rules in SQLite which capture the full
surface area of its language. I&amp;rsquo;d have to specify in each of these &amp;ldquo;grammar
rules&amp;rdquo; how that part of the syntax maps to the matching node in the parse tree.
It&amp;rsquo;s extremely repetitive work; each rule is similar to all the ones around it
but also, by definition, different.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s not just the rules but also coming up with and writing tests to make
sure it&amp;rsquo;s correct, debugging if something is wrong, triaging and fixing the
inevitable bugs people filed when I got something wrong&amp;hellip;&lt;/p&gt;
&lt;p&gt;For years, this was where the idea died. Too hard for a side project&lt;sup class="sn-ref" id="sn-ref-complexity"&gt;&lt;a href="#sn-complexity"&gt;12&lt;/a&gt;&lt;/sup&gt;, too tedious to sustain motivation, too risky to invest months
into something that might not work.&lt;/p&gt;
&lt;h2 id="how-it-happened"&gt;How it happened&lt;a class="heading-anchor" href="#how-it-happened" aria-label="Permalink to How it happened"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been using coding agents since early 2025 (Aider, Roo Code, then Claude
Code since July) and they&amp;rsquo;d definitely been useful but never something I felt I
could trust a serious project to. But towards the end of 2025, the models seemed
to make a significant step forward in quality&lt;sup class="sn-ref" id="sn-ref-agents-got-good"&gt;&lt;a href="#sn-agents-got-good"&gt;13&lt;/a&gt;&lt;/sup&gt;. At the
same time, I kept hitting problems in Perfetto which would have been trivially
solved by having a reliable parser. Each workaround left the same thought in the
back of my mind: maybe it&amp;rsquo;s finally time to build it for real.&lt;/p&gt;
&lt;p&gt;I got some space to think and reflect over Christmas and decided to really
stress test the most maximalist version of AI: could I vibe-code the whole thing
using just Claude Code on the Max plan (£200/month)?&lt;/p&gt;
&lt;p&gt;Through most of January, I iterated, acting as semi-technical manager and
delegating almost all the design and all the implementation to Claude.
Functionally, I ended up in a reasonable place: a parser in C extracted from
SQLite sources using a bunch of Python scripts, a formatter built on top,
support for both the SQLite language and the PerfettoSQL extensions, all exposed
in a web playground.&lt;/p&gt;
&lt;p&gt;But when I reviewed the codebase in detail in late January, the downside was
obvious: the codebase was complete spaghetti&lt;sup class="sn-ref" id="sn-ref-spaghetti"&gt;&lt;a href="#sn-spaghetti"&gt;14&lt;/a&gt;&lt;/sup&gt;. I didn&amp;rsquo;t
understand large parts of the Python source extraction pipeline, functions were
scattered in random files without a clear shape, and a few files had grown to
several thousand lines. It was &lt;em&gt;extremely&lt;/em&gt; fragile; it solved the immediate
problem &lt;em&gt;but&lt;/em&gt; it was never going to cope with my larger vision, never mind
integrating it into the Perfetto tools. The saving grace was that it had proved
the approach was viable and generated more than 500 tests, many of which I felt
I could reuse.&lt;/p&gt;
&lt;p&gt;I decided to throw away everything and start from scratch while also switching
most of the codebase to Rust&lt;sup class="sn-ref" id="sn-ref-rust-not-c"&gt;&lt;a href="#sn-rust-not-c"&gt;15&lt;/a&gt;&lt;/sup&gt;. I could see that C was going
to make it difficult to build the higher level components like the validator and
the language server implementation. And as a bonus, it would also let me use the
same language for both the extraction and runtime instead of splitting it across
C and Python.&lt;/p&gt;
&lt;p&gt;More importantly, I completely changed my role in the project. I took ownership
of all decisions&lt;sup class="sn-ref" id="sn-ref-took-control"&gt;&lt;a href="#sn-took-control"&gt;16&lt;/a&gt;&lt;/sup&gt; and used it more as &amp;ldquo;autocomplete on
steroids&amp;rdquo; inside a much tighter process: opinionated design upfront, reviewing
every change thoroughly, fixing problems eagerly as I spotted them, and
investing in scaffolding (like linting, validation, and non-trivial
testing&lt;sup class="sn-ref" id="sn-ref-scaffolding"&gt;&lt;a href="#sn-scaffolding"&gt;17&lt;/a&gt;&lt;/sup&gt;) to check AI output automatically.&lt;/p&gt;
&lt;p&gt;The core features came together through February and the final stretch (upstream
test validation, editor extensions, packaging, docs) led to a 0.1 launch in
mid-March.&lt;/p&gt;
&lt;p&gt;But in my opinion, this timeline is the least interesting part of this story.
What I really want to talk about is what wouldn&amp;rsquo;t have happened without AI and
also the toll it took on me as I used it.&lt;/p&gt;
&lt;h2 id="ai-is-why-this-project-exists-and-why-its-as-complete-as-it-is"&gt;AI is why this project exists, and why it&amp;rsquo;s as complete as it is&lt;a class="heading-anchor" href="#ai-is-why-this-project-exists-and-why-its-as-complete-as-it-is" aria-label="Permalink to AI is why this project exists, and why it’s as complete as it is"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;h3 id="overcoming-inertia"&gt;Overcoming inertia&lt;a class="heading-anchor" href="#overcoming-inertia" aria-label="Permalink to Overcoming inertia"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve &lt;a href="https://lalitm.com/llm-motivation-via-emotions/"&gt;written in the past&lt;/a&gt;
about how one of my biggest weaknesses as a software engineer is my tendency to
procrastinate when facing a big new project. Though I didn&amp;rsquo;t realize it at the
time, it could not have applied more perfectly to building syntaqlite.&lt;/p&gt;
&lt;p&gt;AI basically let me put aside all my doubts on technical calls, my uncertainty
of building the right thing and my reluctance to get started by giving me very
concrete problems to work on. Instead of &amp;ldquo;I need to understand how SQLite&amp;rsquo;s
parsing works&amp;rdquo;, it was &amp;ldquo;I need to get AI to suggest an approach for me so I can
tear it up and build something better&amp;quot;&lt;sup class="sn-ref" id="sn-ref-inertia-journal"&gt;&lt;a href="#sn-inertia-journal"&gt;18&lt;/a&gt;&lt;/sup&gt;. I work so much
better with concrete prototypes to play with and code to look at than endlessly
thinking about designs in my head, and AI lets me get to that point at a pace I
could not have dreamed about before. Once I took the first step, every step
after that was so much easier.&lt;/p&gt;
&lt;h3 id="faster-at-churning-code"&gt;Faster at churning code&lt;a class="heading-anchor" href="#faster-at-churning-code" aria-label="Permalink to Faster at churning code"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;AI turned out to be better than me at the act of writing code itself, assuming
that code is obvious. If I can break a problem down to &amp;ldquo;write a function with
this behaviour and parameters&amp;rdquo; or &amp;ldquo;write a class matching this interface,&amp;rdquo; AI
will build it faster than I would and, crucially, in a style that might well be
more intuitive to a future reader. It documents things I&amp;rsquo;d skip, lays out code
consistently with the rest of the project, and sticks to what you might call the
&amp;ldquo;standard dialect&amp;rdquo; of whatever language you&amp;rsquo;re working
in&lt;sup class="sn-ref" id="sn-ref-standard-dialect"&gt;&lt;a href="#sn-standard-dialect"&gt;19&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;That standardness is a double-edged sword. For the vast majority of code in any
project, standard is exactly what you want: predictable, readable, unsurprising.
But every project has pieces that are its edge, the parts where the value comes
from doing something non-obvious. For syntaqlite, that was the extraction
pipeline and the parser architecture. AI&amp;rsquo;s instinct to normalize was actively
harmful there, and those were the parts I had to design in depth and often
resorted to just writing myself.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s the flip side: the same speed that makes AI great at obvious code
also makes it great at refactoring. If you&amp;rsquo;re using AI to generate code at
industrial scale, you &lt;em&gt;have&lt;/em&gt; to refactor constantly and
continuously&lt;sup class="sn-ref" id="sn-ref-refactoring-journal"&gt;&lt;a href="#sn-refactoring-journal"&gt;20&lt;/a&gt;&lt;/sup&gt;. If you don&amp;rsquo;t, things immediately get
out of hand. This was the central lesson of the vibe-coding month: I didn&amp;rsquo;t
refactor enough, the codebase became something I couldn&amp;rsquo;t reason about, and I
had to throw it all away. In the rewrite, refactoring became the core of my
workflow. After every large batch of generated code, I&amp;rsquo;d step back and ask &amp;ldquo;is
this ugly?&amp;rdquo; Sometimes AI could clean it up. Other times there was a large-scale
abstraction that AI couldn&amp;rsquo;t see but I could; I&amp;rsquo;d give it the direction and let
it execute&lt;sup class="sn-ref" id="sn-ref-refactor-pattern"&gt;&lt;a href="#sn-refactor-pattern"&gt;21&lt;/a&gt;&lt;/sup&gt;. If you have taste, the cost of a wrong
approach drops dramatically because you can restructure
quickly&lt;sup class="sn-ref" id="sn-ref-refactor-taste"&gt;&lt;a href="#sn-refactor-taste"&gt;22&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id="teaching-assistant"&gt;Teaching assistant&lt;a class="heading-anchor" href="#teaching-assistant" aria-label="Permalink to Teaching assistant"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Of all the ways I used AI, research had by far the highest ratio of value
delivered to time spent.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve worked with interpreters and parsers before but I had never heard of
Wadler-Lindig pretty printing&lt;sup class="sn-ref" id="sn-ref-wadler-lindig"&gt;&lt;a href="#sn-wadler-lindig"&gt;23&lt;/a&gt;&lt;/sup&gt;. When I needed to build
the formatter, AI gave me a concrete and actionable lesson from a point of view
I could understand and pointed me to the papers to learn more. I could have
found this myself eventually, but AI compressed what might have been a day or
two of reading into a focused conversation where I could ask &amp;ldquo;but why does this
work?&amp;rdquo; until I actually got it.&lt;/p&gt;
&lt;p&gt;This extended to entire domains I&amp;rsquo;d never worked in. I have deep C++ and Android
performance expertise but had barely touched Rust tooling or editor extension
APIs. With AI, it wasn&amp;rsquo;t a problem: the fundamentals are the same, the
terminology is similar, and AI bridges the gap&lt;sup class="sn-ref" id="sn-ref-lateral-moves"&gt;&lt;a href="#sn-lateral-moves"&gt;24&lt;/a&gt;&lt;/sup&gt;. The VS
Code extension would have taken me a day or two of learning the API before I
could even start. With AI, I had a working extension within an hour.&lt;/p&gt;
&lt;p&gt;It was also invaluable for reacquainting myself with parts of the project I
hadn&amp;rsquo;t looked at for a few days&lt;sup class="sn-ref" id="sn-ref-context-reacquisition"&gt;&lt;a href="#sn-context-reacquisition"&gt;25&lt;/a&gt;&lt;/sup&gt;. I could control
how deep to go: &amp;ldquo;tell me about this component&amp;rdquo; for a surface-level refresher,
&amp;ldquo;give me a detailed linear walkthrough&amp;rdquo; for a deeper dive, &amp;ldquo;audit unsafe usages
in this repo&amp;rdquo; to go hunting for problems. When you&amp;rsquo;re context switching a lot,
you lose context fast. AI let me reacquire it on demand.&lt;/p&gt;
&lt;h3 id="more-than-id-have-built-alone"&gt;More than I&amp;rsquo;d have built alone&lt;a class="heading-anchor" href="#more-than-id-have-built-alone" aria-label="Permalink to More than I’d have built alone"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Beyond making the project exist at all, AI is also the reason it shipped as
complete as it did. Every open source project has a long tail of features that
are important but not critical: the things you know theoretically how to do but
keep deprioritizing because the core work is more pressing. For syntaqlite, that
list was long: editor extensions, Python bindings, a WASM playground, a docs
site, packaging for multiple ecosystems&lt;sup class="sn-ref" id="sn-ref-last-mile-list"&gt;&lt;a href="#sn-last-mile-list"&gt;26&lt;/a&gt;&lt;/sup&gt;. AI made these
cheap enough that skipping them felt like the wrong trade-off.&lt;/p&gt;
&lt;p&gt;It also freed up mental energy for UX&lt;sup class="sn-ref" id="sn-ref-ux-focus"&gt;&lt;a href="#sn-ux-focus"&gt;27&lt;/a&gt;&lt;/sup&gt;. Instead of spending
all my time on implementation, I could think about what a user&amp;rsquo;s first
experience should feel like: what error messages would actually help them fix
their SQL, how the formatter output should look by default, whether the CLI
flags were intuitive. These are the things that separate a tool people try once
from one they keep using, and AI gave me the headroom to care about them.
Without AI, I would have built something much smaller, probably no editor
extensions or docs site. AI didn&amp;rsquo;t just make the same project faster. It changed
what the project &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="where-ai-had-its-costs"&gt;Where AI had its costs&lt;a class="heading-anchor" href="#where-ai-had-its-costs" aria-label="Permalink to Where AI had its costs"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;h3 id="the-addiction"&gt;The addiction&lt;a class="heading-anchor" href="#the-addiction" aria-label="Permalink to The addiction"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;There&amp;rsquo;s an uncomfortable parallel between using AI coding tools and playing slot
machines&lt;sup class="sn-ref" id="sn-ref-addiction"&gt;&lt;a href="#sn-addiction"&gt;28&lt;/a&gt;&lt;/sup&gt;. You send a prompt, wait, and either get something
great or something useless. I found myself up late at night wanting to do &amp;ldquo;just
one more prompt,&amp;rdquo; constantly trying AI just to see what would happen even when I
knew it probably wouldn&amp;rsquo;t work. The sunk cost fallacy kicked in too: I&amp;rsquo;d keep at
it even in tasks it was clearly ill-suited for, telling myself &amp;ldquo;maybe if I
phrase it differently this time.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The tiredness feedback loop made it worse&lt;sup class="sn-ref" id="sn-ref-tiredness-loop"&gt;&lt;a href="#sn-tiredness-loop"&gt;29&lt;/a&gt;&lt;/sup&gt;. When I had
energy, I could write precise, well-scoped prompts and be genuinely productive.
But when I was tired, my prompts became vague, the output got worse, and I&amp;rsquo;d try
again, getting more tired in the process. In these cases, AI was probably slower
than just implementing something myself, but it was too hard to break out of the
loop&lt;sup class="sn-ref" id="sn-ref-ai-slower"&gt;&lt;a href="#sn-ai-slower"&gt;30&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id="losing-touch"&gt;Losing touch&lt;a class="heading-anchor" href="#losing-touch" aria-label="Permalink to Losing touch"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Several times during the project, I lost my mental model of the
codebase&lt;sup class="sn-ref" id="sn-ref-losing-touch"&gt;&lt;a href="#sn-losing-touch"&gt;31&lt;/a&gt;&lt;/sup&gt;. Not the overall architecture or how things
fitted together. But the day-to-day details of what lived where, which functions
called which, the small decisions that accumulate into a working system. When
that happened, surprising issues would appear and I&amp;rsquo;d find myself at a total
loss to understand what was going wrong. I hated that feeling.&lt;/p&gt;
&lt;p&gt;The deeper problem was that losing touch created a communication
breakdown&lt;sup class="sn-ref" id="sn-ref-communication-breakdown"&gt;&lt;a href="#sn-communication-breakdown"&gt;32&lt;/a&gt;&lt;/sup&gt;. When you don&amp;rsquo;t have the mental
thread of what&amp;rsquo;s going on, it becomes impossible to communicate meaningfully
with the agent. Every exchange gets longer and more verbose. Instead of &amp;ldquo;change
FooClass to do X,&amp;rdquo; you end up saying &amp;ldquo;change the thing which does Bar to do X&amp;rdquo;.
Then the agent has to figure out what Bar is, how that maps to FooClass, and
sometimes it gets it wrong&lt;sup class="sn-ref" id="sn-ref-manager-analogy"&gt;&lt;a href="#sn-manager-analogy"&gt;33&lt;/a&gt;&lt;/sup&gt;. It&amp;rsquo;s exactly the same
complaint engineers have always had about managers who don&amp;rsquo;t understand the code
asking for fanciful or impossible things. Except now you&amp;rsquo;ve become that manager.&lt;/p&gt;
&lt;p&gt;The fix was deliberate: I made it a habit to read through the code immediately
after it was implemented and actively engage to see &amp;ldquo;how would I have done this
differently?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Of course, in some sense all of the above is also true of code I wrote a few
months ago (hence the
&lt;a href="https://text-incubation.com/AI+code+is+legacy+code+from+day+one"&gt;sentiment that AI code is legacy code&lt;/a&gt;),
but AI makes the drift happen faster because you&amp;rsquo;re not building the same muscle
memory that comes from originally typing it out.&lt;/p&gt;
&lt;h3 id="the-slow-corrosion"&gt;The slow corrosion&lt;a class="heading-anchor" href="#the-slow-corrosion" aria-label="Permalink to The slow corrosion"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;There were some other problems I only discovered incrementally over the three
months.&lt;/p&gt;
&lt;p&gt;I found that AI made me procrastinate on key design
decisions&lt;sup class="sn-ref" id="sn-ref-procrastination"&gt;&lt;a href="#sn-procrastination"&gt;34&lt;/a&gt;&lt;/sup&gt;. Because refactoring was cheap, I could
always say &amp;ldquo;I&amp;rsquo;ll deal with this later.&amp;rdquo; And because AI could refactor at the
same industrial scale it generated code, the cost of deferring felt low. But it
wasn&amp;rsquo;t: deferring decisions corroded my ability to think clearly because the
codebase stayed confusing in the meantime. The vibe-coding month was the most
extreme version of this. Yes, I understood the problem, but if I had been more
disciplined about making hard design calls earlier, I could have converged on
the right architecture much faster.&lt;/p&gt;
&lt;p&gt;Tests created a similar false comfort&lt;sup class="sn-ref" id="sn-ref-tests-insufficient"&gt;&lt;a href="#sn-tests-insufficient"&gt;35&lt;/a&gt;&lt;/sup&gt;. Having 500+
tests felt reassuring, and AI made it easy to generate more. But neither humans
nor AI are creative enough to foresee every edge case you&amp;rsquo;ll hit in the future;
there are several times in the vibe-coding phase where I&amp;rsquo;d come up with a test
case and realise the design of some component was completely wrong and needed to
be totally reworked. This was a significant contributor to my lack of trust and
the decision to scrap everything and start from scratch.&lt;/p&gt;
&lt;p&gt;Basically, I learned that the &amp;ldquo;normal rules&amp;rdquo; of software still apply in the AI
age: if you don&amp;rsquo;t have a fundamental foundation (clear architecture,
well-defined boundaries) you&amp;rsquo;ll be left eternally chasing bugs as they appear.&lt;/p&gt;
&lt;h3 id="no-sense-of-time"&gt;No sense of time&lt;a class="heading-anchor" href="#no-sense-of-time" aria-label="Permalink to No sense of time"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Something I kept coming back to was how little AI understood about the passage
of time&lt;sup class="sn-ref" id="sn-ref-no-sense-of-time"&gt;&lt;a href="#sn-no-sense-of-time"&gt;36&lt;/a&gt;&lt;/sup&gt;. It sees a codebase in a certain state but
doesn&amp;rsquo;t &lt;em&gt;feel&lt;/em&gt; time the way humans do. I can tell you what it feels like to use
an API, how it evolved over months or years, why certain decisions were made and
later reversed.&lt;/p&gt;
&lt;p&gt;The natural problem from this lack of understanding is that you either make the
same mistakes you made in the past and have to relearn the lessons &lt;em&gt;or&lt;/em&gt; you fall
into new traps which were successfully avoided the first time, slowing you down
in the long run. In my opinion, this is a similar problem to why losing a
high-quality senior engineer hurts a team so much: they carry history and
context that doesn&amp;rsquo;t exist anywhere else and act as a guide for others around
them.&lt;/p&gt;
&lt;p&gt;In theory, you can try to preserve this context by keeping specs and docs up to
date. But there&amp;rsquo;s a reason we didn&amp;rsquo;t do this before AI: capturing implicit
design decisions exhaustively is incredibly expensive and time-consuming to
write down. AI can help draft these docs, but because there&amp;rsquo;s no way to
automatically verify that it accurately captured what matters, a human still has
to manually audit the result. And that&amp;rsquo;s still time-consuming.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also the context pollution problem. You never know when a design note
about API A will echo in API B. Consistency is a huge part of what makes
codebases work, and for that you don&amp;rsquo;t just need context about what you&amp;rsquo;re
working on right now but also about other things which were designed in a
similar way. Deciding what&amp;rsquo;s relevant requires exactly the kind of judgement
that institutional knowledge provides in the first place.&lt;/p&gt;
&lt;h2 id="relativity"&gt;Relativity&lt;a class="heading-anchor" href="#relativity" aria-label="Permalink to Relativity"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Reflecting on the above, the pattern of when AI helped and when it hurt was
fairly consistent.&lt;/p&gt;
&lt;p&gt;When I was working on something I already understood deeply, AI was excellent. I
could review its output instantly, catch mistakes before they landed and move at
a pace I&amp;rsquo;d never have managed alone. The parser rule generation is the clearest
example&lt;sup class="sn-ref" id="sn-ref-parser-rules"&gt;&lt;a href="#sn-parser-rules"&gt;37&lt;/a&gt;&lt;/sup&gt;: I knew exactly what each rule should produce, so
I could review AI&amp;rsquo;s output within a minute or two and iterate fast.&lt;/p&gt;
&lt;p&gt;When I was working on something I could describe but didn&amp;rsquo;t yet know, AI was
good but required more care. Learning Wadler-Lindig for the formatter was like
this: I could articulate what I wanted, evaluate whether the output was heading
in the right direction, and learn from what AI explained. But I had to stay
engaged and couldn&amp;rsquo;t just accept what it gave me.&lt;/p&gt;
&lt;p&gt;When I was working on something where I didn&amp;rsquo;t even know what I wanted, AI was
somewhere between unhelpful and harmful. The architecture of the project was the
clearest case: I spent weeks in the early days following AI down dead ends,
exploring designs that felt productive in the moment but collapsed under
scrutiny. In hindsight, I have to wonder if it would have been faster just
thinking it through without AI in the loop at all.&lt;/p&gt;
&lt;p&gt;But expertise alone isn&amp;rsquo;t enough. Even when I understood a problem deeply, AI
still struggled if the task had no objectively checkable answer&lt;sup class="sn-ref" id="sn-ref-verifiability"&gt;&lt;a href="#sn-verifiability"&gt;38&lt;/a&gt;&lt;/sup&gt;. Implementation has a right answer, at least at a local level:
the code compiles, the tests pass, the output matches what you asked for. Design
doesn&amp;rsquo;t. We&amp;rsquo;re still arguing about OOP decades after it first took off.&lt;/p&gt;
&lt;p&gt;Concretely, I found that designing the public API of syntaqlite was where this
hit home the hardest. I spent several days in early March doing nothing but API
refactoring, manually fixing things any experienced engineer would have
instinctively avoided but AI made a total mess of. There&amp;rsquo;s no test or objective
metric for &amp;ldquo;is this API pleasant to use&amp;rdquo; and &amp;ldquo;will this API help users solve
the problems they have&amp;rdquo; and that&amp;rsquo;s exactly why the coding agents did &lt;em&gt;so badly&lt;/em&gt;
at it.&lt;/p&gt;
&lt;p&gt;This takes me back to the days I was obsessed with physics and, specifically,
relativity. The laws of physics look simple and Newtonian in any small local
area, but zoom out and spacetime curves in ways you can&amp;rsquo;t predict from the local
picture alone. Code is the same: at the level of a function or a class, there&amp;rsquo;s
usually a clear right answer, and AI is excellent there. But architecture is
what happens when all those local pieces interact, and you can&amp;rsquo;t get good global
behaviour by stitching together locally correct components.&lt;/p&gt;
&lt;p&gt;Knowing where you are on these axes at any given moment is, I think, the core
skill of working with AI effectively.&lt;/p&gt;
&lt;h2 id="wrap-up"&gt;Wrap-up&lt;a class="heading-anchor" href="#wrap-up" aria-label="Permalink to Wrap-up"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Eight years is a long time to carry a project in your head. Seeing these SQLite
tools actually exist and function after only three months of work is a massive
win, and I’m fully aware they wouldn&amp;rsquo;t be here without AI.&lt;/p&gt;
&lt;p&gt;But the process wasn&amp;rsquo;t the clean, linear success story people usually post. I
lost an entire month to vibe-coding. I fell into the trap of managing a codebase
I didn&amp;rsquo;t actually understand, and I paid for that with a total rewrite.&lt;/p&gt;
&lt;p&gt;The takeaway for me is simple: AI is an incredible force multiplier for
implementation, but it’s a dangerous substitute for design. It’s brilliant at
giving you the right answer to a specific technical question, but it has no
sense of history, taste, or how a human will actually feel using your API. If
you rely on it for the &amp;ldquo;soul&amp;rdquo; of your software, you’ll just end up hitting a
wall faster than you ever have before.&lt;/p&gt;
&lt;p&gt;What I&amp;rsquo;d like to see more of from others is exactly what I&amp;rsquo;ve tried to do here:
honest, detailed accounts of building real software with these tools; not
weekend toys or one-off scripts but the kind of software that has to survive
contact with users, bug reports, and your own changing mind.&lt;/p&gt;</content:encoded></item><item><title>syntaqlite: high-fidelity devtools that SQLite deserves</title><link>https://lalitm.com/post/syntaqlite/</link><pubDate>Tue, 17 Mar 2026 05:35:45 +0000</pubDate><guid>https://lalitm.com/post/syntaqlite/</guid><description>Most SQL tools treat SQLite as a “flavor” of a generic SQL parser. They approximate the language, which means they break on SQLite-exclusive features like virtual tables, miss syntax like UPSERT, and ignore the 22 compile-time flags that change the syntax SQLite accepts.
So I built syntaqlite: an open-source parser, formatter, validator, and LSP built directly on SQLite’s own Lemon-generated grammar. It sees SQL exactly how SQLite sees it, no matter which version of SQLite you’re using or which feature flags you compiled with.
It ships as a CLI, VS Code extension, Claude Code LSP plugin, and C/Rust libraries.
There’s also a web playground which you can try now: paste any SQLite SQL and see parsing, formatting, and validation live in the browser, no install needed. Full documentation is available here.
Here’s syntaqlite in action:
Formatting with the CLI
&gt; syntaqlite fmt -e "select u.name,u.email,count(e.id) as events from users u join events e on e.user_id=u.id where u.signed_up_at&gt;=date('now','-30 days') group by u.name,u.email having count(e.id)&gt;10 order by events desc" SELECT u.name, u.email, count(e.id) AS events FROM users AS u JOIN events AS e ON e.user_id = u.id WHERE u.signed_up_at &gt;= date('now', '-30 days') GROUP BY u.name, u.email HAVING count(e.id) &gt; 10 ORDER BY events DESC; Validation with the CLI</description><content:encoded>&lt;p&gt;Most SQL tools treat SQLite as a &amp;ldquo;flavor&amp;rdquo; of a generic SQL parser. They approximate the language, which means they break on SQLite-exclusive features like &lt;a href="https://www.sqlite.org/vtab.html"&gt;virtual tables&lt;/a&gt;, miss syntax like &lt;a href="https://www.sqlite.org/lang_upsert.html"&gt;UPSERT&lt;/a&gt;, and ignore the &lt;a href="https://www.sqlite.org/compile.html"&gt;22 compile-time flags&lt;/a&gt; that change the syntax SQLite accepts.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://github.com/lalitMaganti/syntaqlite"&gt;syntaqlite&lt;/a&gt;: an open-source parser, formatter, validator, and LSP built directly on SQLite&amp;rsquo;s own Lemon-generated grammar. It sees SQL exactly how SQLite sees it, no matter which version of SQLite you&amp;rsquo;re using or which feature flags you compiled with.&lt;/p&gt;
&lt;p&gt;It ships as a &lt;a href="https://docs.syntaqlite.com/main/getting-started/cli"&gt;CLI&lt;/a&gt;, &lt;a href="https://docs.syntaqlite.com/main/getting-started/vscode"&gt;VS Code extension&lt;/a&gt;, &lt;a href="https://docs.syntaqlite.com/main/getting-started/claude-code"&gt;Claude Code LSP plugin&lt;/a&gt;, and &lt;a href="https://docs.syntaqlite.com/main/getting-started/c-parser/"&gt;C&lt;/a&gt;/&lt;a href="https://docs.syntaqlite.com/main/getting-started/rust/"&gt;Rust&lt;/a&gt; libraries.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;strong&gt;&lt;a href="https://playground.syntaqlite.com/"&gt;web playground&lt;/a&gt;&lt;/strong&gt; which you can try now: paste any SQLite SQL and see parsing, formatting, and validation live in the browser, no install needed. &lt;a href="https://docs.syntaqlite.com/"&gt;Full documentation is available here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s syntaqlite in action:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Formatting with the CLI&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; syntaqlite fmt -e &lt;span class="s2"&gt;&amp;#34;select u.name,u.email,count(e.id) as events from users u join events e on e.user_id=u.id where u.signed_up_at&amp;gt;=date(&amp;#39;now&amp;#39;,&amp;#39;-30 days&amp;#39;) group by u.name,u.email having count(e.id)&amp;gt;10 order by events desc&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signed_up_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;now&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-30 days&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;HAVING&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Validation with the CLI&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; syntaqlite --sqlite-version 3.37.0 validate &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -e &lt;span class="s2"&gt;&amp;#34;SELECT json_extract(data, &amp;#39;&lt;/span&gt;$&lt;span class="s2"&gt;.name&amp;#39;) FROM events;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: unknown function &amp;#39;json_extract&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --&amp;gt; &amp;lt;expression&amp;gt;:1:8
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1 | SELECT json_extract(data, &amp;#39;$.name&amp;#39;) FROM events;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ^~~~~~~~~~~~
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;LSP support in VSCode&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://lalitm.com/img/syntaqlite-vscode.png" alt="VS Code showing syntaqlite catching a misspelled column name"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;LSP in Claude Code&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://lalitm.com/img/syntaqlite-cc.png" alt="Claude Code using syntaqlite to catch and fix column name errors"&gt;&lt;/p&gt;
&lt;h2 id="why-it-exists"&gt;Why it exists&lt;a class="heading-anchor" href="#why-it-exists" aria-label="Permalink to Why it exists"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I maintain &lt;a href="https://perfetto.dev/docs/analysis/perfetto-sql-syntax"&gt;PerfettoSQL&lt;/a&gt;, a dialect of SQLite SQL used for trace analysis in &lt;a href="https://perfetto.dev/"&gt;Perfetto&lt;/a&gt;. Across Google there are 200K+ lines of the language. At that scale, you hit &lt;em&gt;every&lt;/em&gt; corner of it. I tried every SQLite tool I could find and time after time I was disappointed: false positives on valid syntax, misparsed statements, formatters that silently corrupted the SQL by changing semantics. It always came down to a hand-written or generic grammar approximating SQLite rather than matching it to the letter.&lt;/p&gt;
&lt;p&gt;Moreover, SQLite&amp;rsquo;s SQL is not one fixed language. Apart from flags that change the syntax, there are another 12 flags that gate &lt;a href="https://www.sqlite.org/compile.html"&gt;built-in functions&lt;/a&gt;, as well as APIs for programs to &lt;a href="https://www.sqlite.org/c3ref/create_function.html"&gt;register custom functions&lt;/a&gt; and &lt;a href="https://www.sqlite.org/vtab.html"&gt;virtual tables&lt;/a&gt;. In Perfetto, we make &lt;em&gt;extensive&lt;/em&gt; use of both virtual tables and functions and most tools just don&amp;rsquo;t give us the flexibility to treat these as first-class extensions of the language.&lt;/p&gt;
&lt;p&gt;The SQLite language also constantly evolves: in the last several years, 3.25.0 added window functions, 3.35.0 added &lt;code&gt;RETURNING&lt;/code&gt;, 3.38.0 added built-in JSON functions to the amalgamation. And because SQLite is embedded, you can&amp;rsquo;t assume everyone is on the latest version. This is certainly the case for Perfetto: people link against different versions of SQLite and we need to make sure our standard library is resilient to that.&lt;/p&gt;
&lt;p&gt;Now with all of this said, I could have built all these tools just for PerfettoSQL and called it a day. But the more I thought about it, the more I felt that the difference between &amp;ldquo;good tooling for PerfettoSQL&amp;rdquo; and &amp;ldquo;good tooling for SQLite&amp;rdquo; is so small that it&amp;rsquo;s worth solving for the wider use case. SQLite is ubiquitous, yet it still lacks the quality tooling other languages have had for over a decade.&lt;/p&gt;
&lt;p&gt;Android is one case I think could especially benefit. SQLite is integral to almost every Android app not to mention the platform itself. But the ecosystem is also famously fragmented: if you&amp;rsquo;re an Android app developer, you &lt;strong&gt;need&lt;/strong&gt; to reason about older platform versions, which in turn means older versions of SQLite.&lt;/p&gt;
&lt;p&gt;As an example, Android 13 ships with SQLite 3.32 (&lt;a href="https://developer.android.com/reference/android/database/sqlite/package-summary"&gt;link&lt;/a&gt;). If you use the &lt;code&gt;RETURNING&lt;/code&gt; keyword and your users are on Android 13, it will fail at runtime even though it passes fine on your test device. Wouldn&amp;rsquo;t it be nice to statically verify this on your host instead of discovering it on a real device?&lt;/p&gt;
&lt;p&gt;No tool I found handled all of this: the precise SQLite grammar, version matching, compile flags, dialect extensions. And that&amp;rsquo;s exactly why syntaqlite exists.&lt;/p&gt;
&lt;h2 id="highlights"&gt;Highlights&lt;a class="heading-anchor" href="#highlights" aria-label="Permalink to Highlights"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;h3 id="validate-semantics-with-great-error-messages"&gt;Validate semantics with great error messages&lt;a class="heading-anchor" href="#validate-semantics-with-great-error-messages" aria-label="Permalink to Validate semantics with great error messages"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Parsing alone is table stakes. But I also wanted to catch common mistakes like reaching for &lt;code&gt;user_name&lt;/code&gt; when the column is actually &lt;code&gt;username&lt;/code&gt;. For this reason, syntaqlite also resolves table, column, and function references, catching errors like misspelled column names or references to tables that don&amp;rsquo;t exist.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also tried to make the error messages &lt;em&gt;really&lt;/em&gt; good: at the quality level you would expect to see from modern tools in other languages like rustc or tsc. This means both accurate, line/column level indication with snippets of the error but also &amp;ldquo;did you mean&amp;rdquo; hints, pointing you in the right direction. The error messages should be &lt;em&gt;significantly&lt;/em&gt; better than SQLite&amp;rsquo;s and it&amp;rsquo;s something I plan on making even better with time.&lt;/p&gt;
&lt;p&gt;As for correctness, I&amp;rsquo;ve tested syntaqlite against ~396K statements from &lt;a href="https://sqlite.org/testing.html"&gt;SQLite&amp;rsquo;s upstream test suite&lt;/a&gt; with ~99.7% agreement on parse acceptance &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. Before we get to 1.0, I fully expect this to reach 100%.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;a href="https://docs.syntaqlite.com/main/reference/comparison/"&gt;detailed comparison&lt;/a&gt; against other tools covering parser accuracy, formatter correctness, validator quality, and performance. Of course the benchmarks and stress tests were written by me, so take it with a grain of salt. But hopefully the results speak for themselves.&lt;/p&gt;
&lt;h3 id="lsp-for-editors-and-coding-agents"&gt;LSP for editors and coding agents&lt;a class="heading-anchor" href="#lsp-for-editors-and-coding-agents" aria-label="Permalink to LSP for editors and coding agents"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;syntaqlite ships a full language server: completions, hover, go-to-definition, find references, rename, diagnostics, and formatting. Unlike most languages, SQL queries depend on an external schema that isn&amp;rsquo;t in the source file. The LSP needs to know about that schema to give you useful diagnostics.&lt;/p&gt;
&lt;p&gt;The most common way to provide this is with a &lt;code&gt;syntaqlite.toml&lt;/code&gt; at the root of your project &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. Point it at your &lt;code&gt;CREATE TABLE&lt;/code&gt; definitions (which you can dump using &lt;code&gt;.schema&lt;/code&gt; on your SQLite database; see the &lt;a href="https://docs.syntaqlite.com/main/guides/project-setup/"&gt;project setup guide&lt;/a&gt; for details) and syntaqlite uses those for autocompletion, diagnostics, and find references.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;**/*.sql&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;db/schema.sql&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells syntaqlite: for every &lt;code&gt;.sql&lt;/code&gt; file in the project, validate against the definitions in &lt;code&gt;db/schema.sql&lt;/code&gt;. You can point at multiple schema files, and scope different schemas to different directories.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;syntaqlite.toml&lt;/code&gt; can be used to configure other preferences like indentation for formatting and the SQLite version as well. The idea is that you check this into your repo and the whole team gets the same experience.&lt;/p&gt;
&lt;h3 id="it-validates-sql-inside-other-languages-experimental"&gt;It validates SQL inside other languages (experimental)&lt;a class="heading-anchor" href="#it-validates-sql-inside-other-languages-experimental" aria-label="Permalink to It validates SQL inside other languages (experimental)"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;SQL is unusual in that it often lives &lt;em&gt;inside&lt;/em&gt; other languages: string literals in Python, template literals in TypeScript, raw strings in C++, and so on.&lt;/p&gt;
&lt;p&gt;As an experimental feature (and with caveats!), syntaqlite can extract and validate SQL &lt;em&gt;inside&lt;/em&gt; other languages, even if you have string interpolation. For example, in a Python f-string:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# app.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;SELECT name, ROUDN(score, 2) FROM users WHERE id = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;syntaqlite validate --experimental-lang python app.py
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;warning: unknown function &amp;#39;ROUDN&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --&amp;gt; app.py:4:23
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4 | f&amp;#34;SELECT name, ROUDN(score, 2) FROM users WHERE id = {user_id}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | ^~~~~
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; = help: did you mean &amp;#39;round&amp;#39;?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It handles the &lt;code&gt;{user_id}&lt;/code&gt; interpolation hole and still catches the function typo with a did-you-mean suggestion.&lt;/p&gt;
&lt;h3 id="and-more"&gt;And more&lt;a class="heading-anchor" href="#and-more" aria-label="Permalink to And more"&gt;#&lt;/a&gt;
&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQLite version- and flag-aware.&lt;/strong&gt; Tell syntaqlite which SQLite version and compile-time flags your target has; the formatter, validator, and LSP all respect them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom dialect support.&lt;/strong&gt; Define grammar, formatting, and validation rules, compile to a shared library, load at runtime. I built this for PerfettoSQL, but it works for any engine that extends SQLite&amp;rsquo;s syntax. For example, a mobile analytics engine with custom aggregation functions or a local-first app framework with its own replication primitives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fast.&lt;/strong&gt; The formatter processes 3,500 lines of SQL in ~5ms. Full benchmarks on the &lt;a href="https://docs.syntaqlite.com/main/reference/comparison/"&gt;comparison page&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;a class="heading-anchor" href="#how-it-works" aria-label="Permalink to How it works"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I plan on writing a detailed followup on the technical side, but I did want to give a short summary here.&lt;/p&gt;
&lt;p&gt;Fundamentally syntaqlite is a C/Rust/C sandwich:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+-------------+---------------+--------------+-------+--------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| VS Code | Claude Code | Playground | CLI | C bindings |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Extension | / Agents | (WASM) | | (Go, Kotlin...) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+-------------+---------------+--------------+-------+--------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Language Server (LSP) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+-------------------------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Formatter · Validator · Analyzer (Rust) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+-------------------------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;| Parser · Tokenizer · AST Arena (C, from SQLite) |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;+-------------------------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The bottom layer is C: SQLite&amp;rsquo;s Lemon-generated grammar and tokenizer, plus an arena storing a &lt;a href="https://jhwlr.io/super-flat-ast/"&gt;super flat AST&lt;/a&gt;. This layer has no Rust dependency and can be used entirely on its own, which matters if you&amp;rsquo;re building a database engine or other tool in C/C++ that just needs to parse SQLite SQL &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The middle layer is Rust. It reads the C arena directly with no copying or deserialization. The formatter generates a &lt;a href="https://lindig.github.io/papers/strictly-pretty-2000.pdf"&gt;Wadler-Lindig document&lt;/a&gt; using an AST-walking interpreter; the semantic analyzer walks the same AST, resolving references and producing diagnostics with byte-accurate source locations.&lt;/p&gt;
&lt;p&gt;The top layer is C again, completing the sandwich: the Rust tools are exported back through FFI so C/C++ projects can link syntaqlite as a plain C library. This also makes bindings for other languages possible: Go, Kotlin/Java, and Python are planned before 1.0. Of course, if you&amp;rsquo;re using Rust directly you can just use the Rust library, skipping the top C layer.&lt;/p&gt;
&lt;p&gt;The obvious question I&amp;rsquo;m expecting is &amp;ldquo;why not just write everything in Rust&amp;rdquo;? I wanted the parser to work &lt;em&gt;exactly&lt;/em&gt; as SQLite does, which meant &lt;em&gt;literally&lt;/em&gt; extracting SQLite&amp;rsquo;s own code and adapting it to my needs. And writing it in C with the same constraints as SQLite itself (no dependencies beyond the C standard library) means it can be linked into practically anything: database engines, C++ projects like Perfetto&amp;rsquo;s trace processor, or any environment where SQLite runs.&lt;/p&gt;
&lt;p&gt;On the other hand, I really did not want to write a formatter, semantic analyzer, and LSP in C. While I do like the &amp;ldquo;simplicity&amp;rdquo; of C, writing a full JSON-RPC server and protocol sounds like a nightmare.&lt;/p&gt;
&lt;h2 id="where-this-is-going"&gt;Where this is going&lt;a class="heading-anchor" href="#where-this-is-going" aria-label="Permalink to Where this is going"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;In my head, there&amp;rsquo;s a clear roadmap for the project side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100% upstream parity.&lt;/strong&gt; The ~0.3% gap against SQLite&amp;rsquo;s test suite is mostly from not fully tracking built-in tables and virtual tables. It just needs another round of &amp;ldquo;source code introspection&amp;rdquo; across all the SQLite versions and integrating those tables into syntaqlite&amp;rsquo;s builtin catalog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LSP enhancements.&lt;/strong&gt; Code actions, document symbols, inlay hints for inferred column types. All of these basically making the editor experience even better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Robust embedded SQL.&lt;/strong&gt; The Python/TypeScript support is really just hacked together with regexes as a proof of concept. My goal is to extend this to use proper parser libraries &lt;em&gt;and&lt;/em&gt; add support for more languages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polish and performance.&lt;/strong&gt; The focus for 0.1 was getting the architecture right. There&amp;rsquo;s room to improve performance, API surface, and documentation across the whole stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for the blog, this project was the reason I&amp;rsquo;ve been completely silent over the last month, but now I&amp;rsquo;m excited to start writing a lot more again. For one, there&amp;rsquo;s the technical followup for the decisions I took and what I learned along the way.&lt;/p&gt;
&lt;p&gt;For another, I used coding agents (mainly Claude Code) extensively while building syntaqlite. There were plenty of moments where following them blindly would have sunk the project, but I can also say with certainty that syntaqlite would not exist without them. I want to dive into that tension: what worked, what didn&amp;rsquo;t, and the complicated feelings I came away with.&lt;/p&gt;
&lt;p&gt;So if you work with SQLite SQL at any scale, I&amp;rsquo;d appreciate you giving syntaqlite a try and telling me what breaks! SQLite is a project which famously plans to be &lt;a href="https://sqlite.org/lts.html"&gt;supported until 2050&lt;/a&gt;. My hope is that syntaqlite will be there as a companion every step of the way, making the experience of writing SQLite SQL that much nicer.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/lalitMaganti/syntaqlite"&gt;syntaqlite on GitHub&lt;/a&gt; | &lt;a href="https://docs.syntaqlite.com/"&gt;Docs&lt;/a&gt; | &lt;a href="https://playground.syntaqlite.com/"&gt;Playground&lt;/a&gt;&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Per-statement results are in &lt;a href="https://github.com/lalitMaganti/syntaqlite/tree/main/tests/upstream_baselines"&gt;&lt;code&gt;tests/upstream_baselines/&lt;/code&gt;&lt;/a&gt;. The remaining 0.3% is largely down to not perfectly matching the builtin tables and then edge cases around where and when column aliases come into scope: it&amp;rsquo;s actually really complicated!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;You can also pass schema files directly via &lt;code&gt;--schema&lt;/code&gt; on the command line without needing a config file.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;The plan is to replace the PerfettoSQL parser in Perfetto&amp;rsquo;s trace processor with this C layer.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>What Makes a Good Tool for Claude Code</title><link>https://lalitm.com/writing-tools-for-claude-code/</link><pubDate>Sun, 12 Oct 2025 00:00:00 +0000</pubDate><guid>https://lalitm.com/writing-tools-for-claude-code/</guid><description>I’ve been using Claude Code extensively for personal projects, and similar AI coding tools at work. Recently I came across this excellent blog post that resonated with a lot of my experience.
One part stuck with me though: Noah emphasizes that tools fail with LLMs when they’re “overly complex,” with the Unix philosophy being particularly well-suited for tool calling. But then I thought about git.
Git breaks the Unix philosophy completely. It’s sprawling, stateful, and complex. And yet Claude Code handles it effortlessly. It composes commands that, even after 10+ years of daily git usage, I wouldn’t think to use. It handles rebasing, cherry-picking, complex resets—stuff that trips up experienced developers regularly.
So if simplicity and the Unix philosophy aren’t the whole story, what else matters?
I’ve come up with three “hallmarks” of a good tool for tool calling with LLMs.
1. It’s been around for a long time and/or is used by lots of people</description><content:encoded>&lt;p&gt;I&amp;rsquo;ve been using Claude Code extensively for personal projects, and similar AI
coding tools at work. Recently I came across
&lt;a href="https://www.alephic.com/writing/the-magic-of-claude-code"&gt;this excellent blog post&lt;/a&gt;
that resonated with a lot of my experience.&lt;/p&gt;
&lt;p&gt;One part stuck with me though: Noah emphasizes that tools fail with LLMs when
they&amp;rsquo;re &amp;ldquo;overly complex,&amp;rdquo; with the Unix philosophy being particularly
well-suited for tool calling. But then I thought about &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Git breaks the Unix philosophy completely. It&amp;rsquo;s sprawling, stateful, and
complex. And yet Claude Code handles it effortlessly. It composes commands that,
even after 10+ years of daily git usage, I wouldn&amp;rsquo;t think to use. It handles
rebasing, cherry-picking, complex resets—stuff that trips up experienced
developers regularly.&lt;/p&gt;
&lt;p&gt;So if simplicity and the Unix philosophy aren&amp;rsquo;t the whole story, what else
matters?&lt;/p&gt;
&lt;p&gt;I’ve come up with three “hallmarks” of a good tool for tool calling with LLMs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. It’s been around for a long time and/or is used by lots of people&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Examples: Unix tools like &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;—but also &lt;code&gt;git&lt;/code&gt;,
&lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt;. Every Stack Overflow thread, blog post, and tutorial
using these tools has likely ended up in the training data. Claude isn’t
reasoning from first principles—it’s drawing on millions of examples.&lt;/p&gt;
&lt;p&gt;This is why git works despite its complexity: Claude has effectively memorized
decades of collective wisdom.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. It has really good documentation (built-in help or external docs)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even if a tool isn’t widely used, great documentation can bridge the gap. I’ve
been building a finance system on top of
&lt;a href="https://beancount.github.io/docs/"&gt;Beancount&lt;/a&gt;—a double-entry accounting system
that’s definitely not mainstream (maybe I’ll write a post about this in the
future). Claude Code handles it surprisingly well because Beancount has
exceptional documentation. When I point Claude at the docs, it can figure out
the directive syntax, transaction formats, and account structures without
necessarily having seen millions of examples in its training data.&lt;/p&gt;
&lt;p&gt;Good &lt;code&gt;--help&lt;/code&gt; text matters. Clear external documentation matters. If Claude can
discover how your tool works, it can use it effectively.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Good error messages&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Error messages, especially those with suggestions like “you used X, did you mean
Y?”, can be tremendously helpful for LLMs. The best example is the Rust
compiler: it gives errors like “you typed foobar, did you mean foobaz?” and
Claude Code can actually use that feedback to correct itself.&lt;/p&gt;
&lt;p&gt;This might be one reason why people feel Claude Code is particularly good at
Rust programming—the compiler is essentially coaching it through mistakes in
real time.&lt;/p&gt;
&lt;p&gt;This isn’t to say the Unix philosophy is wrong — it’s that Unix tools work well
with Claude Code for different reasons than we might think. Tools like sed and
awk nail hallmarks 1 and 2: they’ve been around for decades (massive training
data) and have extensive man pages (great documentation). The fact that they
follow “do one thing well” is almost incidental to their success with LLMs.&lt;/p&gt;
&lt;p&gt;So if I’m building a tool today, I can’t make it instantly popular, but I can
make it understandable. That means good documentation, clear error messages, and
a few solid examples of how it’s used. Those aren’t new ideas — they’ve always
mattered. The difference now is that there’s much higher ROI in doing them than
there used to be.&lt;/p&gt;
&lt;p&gt;In other words, we used to write docs for humans. Now, we’re also writing for
AIs that help humans. That shift changes the economics of good documentation in
a big way.&lt;/p&gt;</content:encoded></item></channel></rss>