<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>nlqdb blog</title>
    <link>https://nlqdb.com/blog/</link>
    <description>Engineering notes from building nlqdb — SQL traps, LLM-pipeline debugging, honest comparisons.</description>
    <language>en-us</language>
    <atom:link href="https://nlqdb.com/rss.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Wed, 15 Jul 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>The redesign shipped. The smoke test kept walking the old UI.</title>
      <link>https://nlqdb.com/blog/smoke-test-walks-the-old-ui/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/smoke-test-walks-the-old-ui/</guid>
      <pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
      <description>Acceptance walkers that pin literal UI copy catch regressions — until a redesign turns them into a 0/9 that mixes real breakage with pure test-drift. The triage cost is the trap, not the literals.</description>
    </item>
    <item>
      <title>Your recovery code runs once. Your failure doesn't.</title>
      <link>https://nlqdb.com/blog/one-shot-recovery-permanent-outage/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/one-shot-recovery-permanent-outage/</guid>
      <pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate>
      <description>A best-effort repair that runs once turns one silent skip into a permanent outage. Fix the root, keep it idempotent, and re-trigger from the steady-state symptom — the event never recurs.</description>
    </item>
    <item>
      <title>A green checkmark has a half-life.</title>
      <link>https://nlqdb.com/blog/green-checkmark-has-a-half-life/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/green-checkmark-has-a-half-life/</guid>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
      <description>When an expensive test suite can't run on every push, passing stops being a state and becomes an event. Score each suite pass × freshness with a linear decay so the number rots until it re-runs.</description>
    </item>
    <item>
      <title>We rebuilt staging's database every run. The registry remembered everything.</title>
      <link>https://nlqdb.com/blog/ephemeral-staging-persistent-registry/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/ephemeral-staging-persistent-registry/</guid>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
      <description>An environment is only as ephemeral as the most persistent store that references it. Enumerate every store that outlives the rebuild and reset it at spin-up — teardown can't be the invariant.</description>
    </item>
    <item>
      <title>Ownership transfer was a one-row UPDATE. Then we added least-privilege.</title>
      <link>https://nlqdb.com/blog/ownership-transfer-outlives-least-privilege/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/ownership-transfer-outlives-least-privilege/</guid>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
      <description>Hardening queries to per-tenant roles and RLS quietly broke our ownership transfer: it retargeted one authorization store out of four. Transfer must move them all — idempotently, in one batch.</description>
    </item>
    <item>
      <title>Your most active user is your test suite.</title>
      <link>https://nlqdb.com/blog/most-active-user-is-your-test-suite/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/most-active-user-is-your-test-suite/</guid>
      <pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate>
      <description>Pre-launch, synthetic traffic IS your traffic: e2e walkers register users and run real queries, so every dashboard quietly measures your robots. Three places it bit us, three fixes.</description>
    </item>
    <item>
      <title>Your five fallback models are one point of failure.</title>
      <link>https://nlqdb.com/blog/five-fallback-models-one-provider/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/five-fallback-models-one-provider/</guid>
      <pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate>
      <description>A model-diverse fallback list on one gateway saturates as a unit — five models, one rack. Make the fallback unit a lane (base URL, key, candidates), not a longer list on the same pool.</description>
    </item>
    <item>
      <title>An &quot;open question&quot; that's already decided is worse than one that's still open.</title>
      <link>https://nlqdb.com/blog/decided-questions-rot-in-your-decision-log/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/decided-questions-rot-in-your-decision-log/</guid>
      <pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate>
      <description>Decision logs rot at the seam between open and answered: a decided-but-unmarked bullet makes readers re-litigate closed calls. Make resolved a greppable state and count unmarked bullets as debt.</description>
    </item>
    <item>
      <title>Your metric is only as honest as the layer you emit it from.</title>
      <link>https://nlqdb.com/blog/emit-metrics-where-the-distinction-is-certain/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/emit-metrics-where-the-distinction-is-certain/</guid>
      <pubDate>Sat, 11 Jul 2026 00:00:00 GMT</pubDate>
      <description>A destructive-op retry rate emitted at the HTTP route can go negative — the route can't tell reads from writes. Emit metrics at the lowest layer where the distinction is certain.</description>
    </item>
    <item>
      <title>You need to rotate an encryption key. You don't need a key-version column.</title>
      <link>https://nlqdb.com/blog/rotate-encryption-key-without-a-version-column/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/rotate-encryption-key-without-a-version-column/</guid>
      <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
      <description>Rotating a key-encryption key feels like it needs a key_version column. It doesn't — the ciphertext already describes itself, so put the version in the blob prefix and rotate with zero migration.</description>
    </item>
    <item>
      <title>You added a second SQL engine. Your text-to-SQL model is still being told it's the first one.</title>
      <link>https://nlqdb.com/blog/text-to-sql-planner-told-wrong-dialect/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/text-to-sql-planner-told-wrong-dialect/</guid>
      <pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate>
      <description>A text-to-SQL planner emits whatever dialect you name it. Add a second engine and the bug is one hardcoded dialect literal the type never forced you to fix — so ClickHouse gets Postgres SQL.</description>
    </item>
    <item>
      <title>You added ClickHouse. Your Postgres SQL validator now rejects valid queries — quietly.</title>
      <link>https://nlqdb.com/blog/postgres-validator-rejects-valid-clickhouse-sql/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/postgres-validator-rejects-valid-clickhouse-sql/</guid>
      <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
      <description>A Postgres-pinned AST validator false-rejects valid ClickHouse grammar as parse_failed — a silent veto of correct SQL. The fix: split the security allowlist from the dialect parser.</description>
    </item>
    <item>
      <title>We published 20 blog posts and never shipped a feed. Nothing could subscribe.</title>
      <link>https://nlqdb.com/blog/blog-without-a-feed-is-a-dead-end/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/blog-without-a-feed-is-a-dead-end/</guid>
      <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
      <description>A blog with no RSS feed is a one-way street: feed readers and dev.to/Medium 'import from RSS' both need a feed URL. Without one, every venue re-post is a manual copy-paste that quietly stops.</description>
    </item>
    <item>
      <title>We read the agent-memory benchmarks. Almost none measure analysis.</title>
      <link>https://nlqdb.com/blog/agent-memory-benchmarks-measure-recall-not-analysis/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/agent-memory-benchmarks-measure-recall-not-analysis/</guid>
      <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
      <description>Agent-memory benchmarks score end-to-end recall of facts on mostly self-reported numbers. Almost nobody measures analysis over memory — the gap we found reading the papers.</description>
    </item>
    <item>
      <title>We shipped 18 SEO pages and got 1 referral. The links only pointed one way.</title>
      <link>https://nlqdb.com/blog/one-way-internal-links-leak-yield/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/one-way-internal-links-leak-yield/</guid>
      <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
      <description>The page count climbed; referrals stayed flat at ~1/week. Our internal link graph was a tree, not a mesh. The fix was one reciprocal link, derived from a field we already had.</description>
    </item>
    <item>
      <title>Your database scales to zero. Your retry loop doesn't know that.</title>
      <link>https://nlqdb.com/blog/serverless-db-cold-start-retry/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/serverless-db-cold-start-retry/</guid>
      <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
      <description>A scale-to-zero Postgres branch fails the first query while its compute wakes. Instant retries replay the cold connection. The fix: back off the DB stage, not the LLM stages.</description>
    </item>
    <item>
      <title>The timeout that looked like a hallucination</title>
      <link>https://nlqdb.com/blog/llm-timeout-looks-like-hallucination/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/llm-timeout-looks-like-hallucination/</guid>
      <pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
      <description>Our NL→SQL benchmark scored a frontier model as junk on 5 hard questions. It never hallucinated — a 5s prod timeout aborted it mid-answer and the handler mislabeled the abort as a parse failure.</description>
    </item>
    <item>
      <title>Your &quot;best model&quot; toggle quietly serves the cheap model. Ship a 409 instead.</title>
      <link>https://nlqdb.com/blog/model-preset-fail-loud/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/model-preset-fail-loud/</guid>
      <pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
      <description>When the premium lane isn't available, the tempting branch is to silently serve the default chain. A placebo knob is worse than no knob. The honest contract: pin, upgrade, or fail loud.</description>
    </item>
    <item>
      <title>Your LLM health probe passed. Your agent still starved.</title>
      <link>https://nlqdb.com/blog/llm-preflight-probe-health/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/llm-preflight-probe-health/</guid>
      <pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
      <description>Six straight LLM-agent CI runs failed while our pre-flight probe stayed green. Lessons on gating CI on an LLM provider: probe the real shape, read the body not the status, never trust one model.</description>
    </item>
    <item>
      <title>Your text-to-SQL model isn't as wrong as your benchmark says. The gold SQL is.</title>
      <link>https://nlqdb.com/blog/bird-gold-noise-distinct/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/bird-gold-noise-distinct/</guid>
      <pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate>
      <description>We bucketed 238 BIRD-dev losses with a structural differ: 46 differ from gold only by a DISTINCT the model rightly added. Audit gold quality before writing prompt directives, or you overfit to noise.</description>
    </item>
    <item>
      <title>Your LLM fused the two columns you asked for — and the eval marked it wrong</title>
      <link>https://nlqdb.com/blog/llm-concatenates-columns-text-to-sql/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/llm-concatenates-columns-text-to-sql/</guid>
      <pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate>
      <description>Gold SQL returns first_name, last_name as two columns; the model returns one concatenated full name. Positional-tuple EX scoring can never match them, so a semantically right answer scores as a miss.</description>
    </item>
    <item>
      <title>Your text-to-SQL eval is lying: the gateway returns HTTP 200 with the error in the body</title>
      <link>https://nlqdb.com/blog/http-200-error-in-body/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/http-200-error-in-body/</guid>
      <pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate>
      <description>A gateway commits 200 OK before the upstream model fails, so the error rides in the 200 body. A res.ok-only client counts it as a wrong answer, not an outage. res.ok is necessary, not sufficient.</description>
    </item>
    <item>
      <title>Top N per group is the query `LIMIT` can't write</title>
      <link>https://nlqdb.com/blog/top-n-rows-per-group/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/top-n-rows-per-group/</guid>
      <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
      <description>&quot;Top 3 per category&quot; reads like ORDER BY … LIMIT 3, but LIMIT caps the whole result set, not each group. The fix is ROW_NUMBER() OVER (PARTITION BY …) — and the hidden decision is how ties break.</description>
    </item>
    <item>
      <title>Your BI tool got acquired. Your data layer shouldn't have to care.</title>
      <link>https://nlqdb.com/blog/your-bi-tool-got-acquired-data-layer/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/your-bi-tool-got-acquired-data-layer/</guid>
      <pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate>
      <description>BI notebooks get rolled up — Mode → ThoughtSpot, Looker → Google, Periscope → Sisense. Fine when it's a destination humans log into; a liability when your product's runtime calls its API.</description>
    </item>
    <item>
      <title>The duplicate-rows query you re-Google every six weeks</title>
      <link>https://nlqdb.com/blog/find-duplicate-rows-you-re-google-every-time/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/find-duplicate-rows-you-re-google-every-time/</guid>
      <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
      <description>Find duplicates hasn't changed in thirty years: GROUP BY the suspect columns, HAVING COUNT(*) &gt; 1. Wanting the whole row, not just the key, quietly changes it to a window function.</description>
    </item>
    <item>
      <title>The text-to-SQL demo takes an afternoon. The other 90% is why you should buy it.</title>
      <link>https://nlqdb.com/blog/text-to-sql-build-vs-buy/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/text-to-sql-build-vs-buy/</guid>
      <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
      <description>Prompt + model + run the SQL is 10% of an 'ask your data' feature. The fail-closed validator, plan cache, and eval harness are the rest — yours forever. The real question: do you want that stack?</description>
    </item>
    <item>
      <title>Your sitemap is advertising redirects — and your canonical tag points at one</title>
      <link>https://nlqdb.com/blog/sitemap-advertising-redirects/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/sitemap-advertising-redirects/</guid>
      <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
      <description>A static host that serves route/index.html 307-redirects the bare path. Our sitemap advertised 27 redirecting URLs and every canonical tag pointed at one. The fix is one path-normalize helper.</description>
    </item>
    <item>
      <title>Your offline LLM eval isn't measuring your model — it's measuring your rate limits</title>
      <link>https://nlqdb.com/blog/offline-llm-eval-rate-limits/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/offline-llm-eval-rate-limits/</guid>
      <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
      <description>A free-model NL-to-SQL bench scored 17/20, then 6/20 ninety seconds later. The model didn't change — the providers got tired. How to keep availability out of your accuracy number.</description>
    </item>
    <item>
      <title>AI made the internal-tool builder faster. It didn't ask whether you needed the tool.</title>
      <link>https://nlqdb.com/blog/ai-internal-tool-builder-faster/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/ai-internal-tool-builder-faster/</guid>
      <pubDate>Fri, 03 Jul 2026 00:00:00 GMT</pubDate>
      <description>Low-code AI scaffolds the admin tool in a prompt. But the output is still a destination a human operates — and often the answer belongs inline in your product, or the asker is an agent.</description>
    </item>
    <item>
      <title>Your text-to-SQL accuracy is measured on schemas your users will never build</title>
      <link>https://nlqdb.com/blog/text-to-sql-accuracy-schemas-your-users-never-build/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/text-to-sql-accuracy-schemas-your-users-never-build/</guid>
      <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
      <description>BIRD and Spider score NL-to-SQL over messy academic schemas. The same free-model chain that scores 0.52 on BIRD scores 0.96 on the schema shapes our users actually build — so we report both.</description>
    </item>
    <item>
      <title>Every data tool shipped an MCP server this year. Your agent still can't build on most of them.</title>
      <link>https://nlqdb.com/blog/mcp-server-what-does-the-agent-own/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/mcp-server-what-does-the-agent-own/</guid>
      <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
      <description>Two shapes of MCP server look identical in a feature matrix: a window into a human's app, or infrastructure the agent owns. The tell is what the agent owns after the call returns.</description>
    </item>
    <item>
      <title>Your agent's memory is a vector store. Ask it &quot;how many&quot; and watch it fall over.</title>
      <link>https://nlqdb.com/blog/agent-memory-vector-store-aggregation-gap/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/agent-memory-vector-store-aggregation-gap/</guid>
      <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
      <description>A vector store returns the top-k most similar memories — there is no GROUP BY, COUNT, or JOIN. Recall is similarity; reporting is aggregation. Agent memory needs both machines, not one.</description>
    </item>
    <item>
      <title>You don't need a backend to store form submissions. You need a place to ask &quot;how many.&quot;</title>
      <link>https://nlqdb.com/blog/store-form-submissions-without-a-backend/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/store-form-submissions-without-a-backend/</guid>
      <pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate>
      <description>Storing a signup is a trivial insert — no server needed. The part that wants a database is the reporting: &quot;signups per day,&quot; &quot;which referrer converted&quot; — aggregations that want a query planner.</description>
    </item>
    <item>
      <title>NOT IN returned zero rows. It wasn't your data — it was one NULL.</title>
      <link>https://nlqdb.com/blog/not-in-subquery-null-trap/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/not-in-subquery-null-trap/</guid>
      <pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate>
      <description>Why WHERE id NOT IN (SELECT …) silently returns nothing when the subquery contains a NULL, and the two anti-join shapes (NOT EXISTS, LEFT JOIN … IS NULL) that never lie to you.</description>
    </item>
    <item>
      <title>Zep gives my agent perfect recall. It still can't answer &quot;average per group&quot; about its own memory.</title>
      <link>https://nlqdb.com/blog/zep-recall-vs-analytical-agent-memory/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/zep-recall-vs-analytical-agent-memory/</guid>
      <pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate>
      <description>A temporal knowledge graph is genuinely good at recall — and has no query planner. When the question about agent memory is a GROUP BY, retrieval and aggregation are different machines.</description>
    </item>
    <item>
      <title>The NULL timestamp that broke a TTL sweep and a funnel metric at the same time</title>
      <link>https://nlqdb.com/blog/null-timestamp-ttl-sweep-funnel-metric/</link>
      <guid isPermaLink="true">https://nlqdb.com/blog/null-timestamp-ttl-sweep-funnel-metric/</guid>
      <pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate>
      <description>A backfill is not a default: one nullable timestamp column made an age-based eviction a silent no-op and pinned a funnel metric at zero — the same NULL, two different failure modes.</description>
    </item>
  </channel>
</rss>
