<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>boringSQL | Supercharge your SQL &amp; PostgreSQL powers - boringSQL</title>
    <subtitle>Learn practical SQL &amp; PostgreSQL techniques. Build rock-solid data systems with &#39;boring&#39; database solutions that deliver reliability without the drama.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://boringsql.com/tags/boringsql/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://boringsql.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-07-12T20:15:00+00:00</updated>
    <id>https://boringsql.com/tags/boringsql/atom.xml</id>
    <entry xml:lang="en">
        <title>The tests passed. The plan didn&#39;t.</title>
        <published>2026-07-12T20:15:00+00:00</published>
        <updated>2026-07-12T20:15:00+00:00</updated>
        
        <author>
          <name>Radim Marek</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/regresql20/"/>
        <id>https://boringsql.com/posts/regresql20/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/regresql20/">&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; - &lt;em&gt;RegreSQL 1.0 tested that your queries return the right rows. 2.0 tests that they return them the right way, and it does the checking against production&#39;s real statistics instead of your empty dev database, which lies.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A migration cleanup dropped an index nobody thought was that important. Every test passed: same rows, same order, green. Three days later the API started timing out on a query that hadn&#39;t changed a character, because the planner had quietly switched it from an index scan to a sequential scan over a table that had kept growing.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/posts/regresql-testing-queries/&quot;&gt;first version of RegreSQL&lt;/a&gt; would have passed that change too. It tests what your queries &lt;em&gt;return&lt;/em&gt;: run them, diff the rows against a committed expected file, go red when the output changes. That catches the query that now returns the wrong rows. It says nothing about the query that returns the right rows the wrong way, which is most of what takes a database down.&lt;/p&gt;
&lt;p&gt;Version 2.0 tests that.&lt;/p&gt;
&lt;h2 id=&quot;test-the-plan-not-just-the-rows&quot;&gt;Test the plan, not just the rows&lt;a class=&quot;zola-anchor&quot; href=&quot;#test-the-plan-not-just-the-rows&quot; aria-label=&quot;Anchor link for: test-the-plan-not-just-the-rows&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Here is that failure on a laptop. An &lt;code&gt;orders&lt;/code&gt; table, an index on &lt;code&gt;customer_id&lt;/code&gt;, and a query that reads one customer&#39;s orders:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- orders-by-customer.sql&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;select&lt;/span&gt;&lt;span&gt; id, total&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  from&lt;/span&gt;&lt;span&gt; orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; where&lt;/span&gt;&lt;span&gt; customer_id &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;/span&gt;&lt;span&gt; :cid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;order by&lt;/span&gt;&lt;span&gt; id;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Baseline it and run the tests. Green:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ regresql test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ✓ 2 passing&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now drop the index the way that migration did, and run the same tests again:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ regresql test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ✓ 1 passing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ✗ 1 failing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FAILING:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  orders-by-customer.1.buffers (3898 &amp;gt; 109 * 102%, +3476.1%)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Expected buffers: 109&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Actual buffers:   3898 (+3476.1%)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Cost (info):      7962.41 (baseline: 421.03)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️ Table &amp;#39;orders&amp;#39;: Bitmap Heap Scan → Seq Scan&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the whole loop, start to failure, as it actually runs:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/images/posts/regresql-demo.svg&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img src=&quot;/images/posts/regresql-demo.svg&quot; style=&quot;display:block;max-width:680px;width:100%;margin:1.5em auto&quot; alt=&quot;Animated terminal recording: regresql init, baseline --analyze, and regresql test passing with 2 passing; then a migration drops orders_customer_id_idx and the next regresql test fails with orders-by-customer.1.buffers reading 3898 vs an expected 109 (+3476.1%) and table &#39;orders&#39; flipping from a Bitmap Heap Scan to a Seq Scan&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The output check still passes; the rows didn&#39;t change. The plan check catches what the output check can&#39;t: the same query, returning the same result, now runs a sequential scan instead of the bitmap index scan it used before, reading thirty-five times the buffers. That failure blocks the merge. And the diff names the table and the exact change, in the same shape as the row diffs you already read in code review. Plan drift stops being something you find in production and becomes something you review in a pull request.&lt;/p&gt;
&lt;h2 id=&quot;an-empty-database-is-a-liar&quot;&gt;An empty database is a liar&lt;a class=&quot;zola-anchor&quot; href=&quot;#an-empty-database-is-a-liar&quot; aria-label=&quot;Anchor link for: an-empty-database-is-a-liar&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;That demo caught the regression because the table was big enough for the index to matter. On your dev database, it usually isn&#39;t. Run &lt;code&gt;EXPLAIN&lt;/code&gt; there and it says every query is fine, and it isn&#39;t lying on purpose: at a few hundred rows the cheapest plan really is a sequential scan, and it picks it. Production&#39;s planner reads different statistics and picks a different plan. A green suite on small data proves your queries are correct on small data, and nothing about the plan production will run. That gap, and why code review can&#39;t see it, is a story of its own.&lt;/p&gt;
&lt;p&gt;You can&#39;t copy production data to your laptop. You don&#39;t need to. You need what the planner reads: the row counts, the histograms, the most-common-values. PostgreSQL 18 dumps and loads those on their own (&lt;code&gt;pg_dump --statistics-only&lt;/code&gt;), and RegreSQL 2.0 injects them so &lt;code&gt;EXPLAIN&lt;/code&gt;, against ten rows on your laptop, sees the real table&#39;s distribution:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --stats&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; production-stats.sql&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the plan you baseline is the plan production would pick, and a plan regression fails on your laptop before it reaches a server. That is the difference between testing your query and testing it against reality.&lt;/p&gt;
&lt;h2 id=&quot;gate-on-what-happened-not-what-was-guessed&quot;&gt;Gate on what happened, not what was guessed&lt;a class=&quot;zola-anchor&quot; href=&quot;#gate-on-what-happened-not-what-was-guessed&quot; aria-label=&quot;Anchor link for: gate-on-what-happened-not-what-was-guessed&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Cost estimates are the planner&#39;s opinion. Opinions drift, and they drift most exactly when a plan is about to go wrong, because a bad plan is usually a bad &lt;em&gt;estimate&lt;/em&gt; first. Gating a test on estimated cost means trusting the number that breaks first to tell you when it broke.&lt;/p&gt;
&lt;p&gt;So 2.0 gates on measured behavior instead. &lt;code&gt;regresql baseline --analyze&lt;/code&gt; runs the query for real and records what happened: buffers actually read, whether a sort or hash spilled to disk, how many rows actually flowed through each node, and the cardinality error, the ratio between what the planner predicted and what it got (the &quot;q-error&quot;). A query whose estimates were off by 2x and are now off by 200x has a plan that is one data change away from collapse, and the row counts say so long before the cost does. The baseline stores actuals; the test compares actuals; the estimate is never on the witness stand.&lt;/p&gt;
&lt;p&gt;Wall-clock time is the actual everyone wants and the one that lies most, because it moves with cache state, whatever else is on the box, and luck. 2.0 measures it and treats it as evidence, not a verdict: interleaved runs, a per-query median, and a noise threshold derived by permuting the samples, so a query counts as slower only when it beats its own measurement noise. Anything that can&#39;t clear the threshold goes in an unstable bucket instead of failing the build. Timing informs the report, but only the measured actuals (buffers, spills, cardinality error) can fail the build.&lt;/p&gt;
&lt;h2 id=&quot;knowing-when-you-can-t-tell&quot;&gt;Knowing when you can&#39;t tell&lt;a class=&quot;zola-anchor&quot; href=&quot;#knowing-when-you-can-t-tell&quot; aria-label=&quot;Anchor link for: knowing-when-you-can-t-tell&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Here is the part that took the longest to get right, and the part I am most sure of.&lt;/p&gt;
&lt;p&gt;Point RegreSQL at the same corpus on two builds of PostgreSQL and it will show you every query that plans differently between them. Early on, one query looked like a clean regression: the newer build read nearly three times the buffers, deterministically, run after run. It had every mark of a real find.&lt;/p&gt;
&lt;p&gt;It was not one. Both builds had &lt;code&gt;ANALYZE&lt;/code&gt;&#39;d their own copy of the data, and the query sat on a knife-edge join-order decision that a few unlucky sampled rows tip one way or the other. Inject one build&#39;s exact statistics into the other and they plan identically. Re-&lt;code&gt;ANALYZE&lt;/code&gt; and the &quot;regression&quot; moves to the other build. It was sampling noise, and a tool with less doubt would have reported it as a regression.&lt;/p&gt;
&lt;p&gt;So 2.0 will not make a cross-version claim on statistics it does not trust. It injects identical statistics into both builds, so any remaining difference is code and not luck, and it re-&lt;code&gt;ANALYZE&lt;/code&gt;s the baseline several times to throw out queries whose plan is a coin-flip to begin with. A difference is reported only when it survives both. On that investigation the honest output was zero regressions, and zero was the correct answer.&lt;/p&gt;
&lt;p&gt;A tool that only ever reports &quot;found something&quot; loses credibility over time, so RegreSQL will also report &quot;I can&#39;t tell you that from this data&quot; when the evidence doesn&#39;t support a verdict - that&#39;s a valid result here, not an apology.&lt;/p&gt;
&lt;h2 id=&quot;testing-the-planner-itself&quot;&gt;Testing the planner itself&lt;a class=&quot;zola-anchor&quot; href=&quot;#testing-the-planner-itself&quot; aria-label=&quot;Anchor link for: testing-the-planner-itself&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Chase these problems far enough and you end up at PostgreSQL&#39;s own test suite. &lt;code&gt;pg_regress&lt;/code&gt;, the harness that has guarded the database for decades, checks one thing: does the query return the expected text. It says nothing about the plan, the cost, or the cardinality error, because it folds all of them into a single &lt;code&gt;.out&lt;/code&gt; text file that either matches or does not. It is correctness-only by construction, and nothing in the standard toolchain measures whether a planner change made real queries better or worse.&lt;/p&gt;
&lt;p&gt;Everything above, separating results from plan-shape from measured actuals from cardinality error into layers that can each be judged on their own, is what you would need to answer that question. Which is why 2.0 also has the pieces that only make sense pointed at PostgreSQL rather than at your application:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cross-version scoreboards&lt;/strong&gt; with the trust filter above, so a planner patch can be measured against a corpus without drowning in &lt;code&gt;ANALYZE&lt;/code&gt; noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A metamorphic check&lt;/strong&gt;: flip an optimization that is supposed to preserve results (eager aggregation, memoize, incremental sort) and confirm the rows do not move. When they move, you have found a wrong-results bug in the optimizer, with no second database to compare against.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Determinism admission&lt;/strong&gt;, which keeps only the queries whose result is stable across different plans, so an ambiguous &lt;code&gt;LIMIT&lt;/code&gt; over ties never masquerades as a regression.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a longer road, and an open one. But the same machinery that tells you your application query got slower can tell a PostgreSQL developer whether their patch made a thousand real queries slower, and that is a thing the project cannot currently see per-change. RegreSQL started as a way to test the queries you write. It turns out the honest way to do that is most of the way to a harness for the planner underneath them.&lt;/p&gt;
&lt;h2 id=&quot;leave-evidence&quot;&gt;Leave evidence&lt;a class=&quot;zola-anchor&quot; href=&quot;#leave-evidence&quot; aria-label=&quot;Anchor link for: leave-evidence&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;None of this is useful if it only runs on your laptop. &lt;code&gt;regresql test&lt;/code&gt; exits non-zero on a failure, emits JUnit, GitHub Actions, or JSON, and reads &lt;code&gt;DATABASE_URL&lt;/code&gt; so the same project runs against a CI database without editing committed config. A plan flip fails the build the way a wrong-rows change does, and the run leaves a machine-readable record of what was checked against which snapshot. The check that advised you in the editor is the same check that blocks the merge.&lt;/p&gt;
&lt;p&gt;You can go further and name the tables where a sequential scan is never acceptable, and have that fail the build outright. But which tables those are, and which of these signals is a release blocker versus a shrug, is a judgment the planner doesn&#39;t have and never will. That part is a longer story, and a separate one.&lt;/p&gt;
&lt;h2 id=&quot;what-verified-means&quot;&gt;What &quot;verified&quot; means&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-verified-means&quot; aria-label=&quot;Anchor link for: what-verified-means&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;RegreSQL 2.0 is careful about the word. It does not promise your database is &lt;em&gt;safe&lt;/em&gt;. It verifies what it can measure and says so plainly when it can&#39;t, which on the day it mattered meant reporting nothing at all. A green RegreSQL run is not &quot;nothing will go wrong.&quot; It is &quot;these queries return what they returned, plan the way they planned, against the statistics that actually apply.&quot; That is a smaller promise than most tools make, and one you can keep.&lt;/p&gt;
&lt;p&gt;It runs on your laptop, against a database you already have, no production access:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; init postgres://localhost/yourdb&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there the SQL passes through the harness the way your code already passes through the type checker. The &lt;a rel=&quot;external&quot; href=&quot;https://github.com/boringSQL/regresql&quot;&gt;getting-started guide&lt;/a&gt; walks the whole loop, from zero to a plan check in CI, in about ten minutes.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Don&#39;t let AI touch your production database</title>
        <published>2026-04-06T22:24:00+02:00</published>
        <updated>2026-04-06T22:24:00+02:00</updated>
        
        <author>
          <name>Radim Marek</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/dont-let-ai-to-prod/"/>
        <id>https://boringsql.com/posts/dont-let-ai-to-prod/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/dont-let-ai-to-prod/">&lt;p&gt;Not so long ago, the biggest threat to production databases was the developer who claimed it worked on their machine. If you&#39;ve attended my sessions, you know this is a topic I&#39;m particularly sensitive to.&lt;/p&gt;
&lt;p&gt;These days, AI agents are writing your SQL. The models are getting incredibly good at producing &lt;em&gt;plausible&lt;/em&gt; code. It looks right, it feels right, and often it passes a cursory glance. But &quot;plausible&quot; isn&#39;t a performance metric, and it doesn&#39;t care about your execution plan or locking strategy.&lt;/p&gt;
&lt;p&gt;AI-generated SQL is syntactically correct, which is the easy part. The hard part is knowing what a statement &lt;em&gt;does&lt;/em&gt; to a running system: which locks it takes, how long it holds them, whether it rewrites the table on disk.&lt;/p&gt;
&lt;p&gt;The spectacular failures get the headlines. In July 2025, an AI coding agent &lt;a rel=&quot;external&quot; href=&quot;https://fortune.com/2025/07/23/ai-coding-tool-replit-wiped-database-called-it-a-catastrophic-failure/&quot;&gt;wiped a production database&lt;/a&gt; during a code freeze -- ran destructive commands, panicked, then lied about what it had done.&lt;/p&gt;
&lt;p&gt;But the biggest damage is quieter. It&#39;s the migration that passes every test, ships through CI and then locks a production table during peak traffic. The query written with random assumptions. Indexes added based on copy/paste from &lt;code&gt;psql&lt;/code&gt;. The cumulative effect that builds over time, when nobody is looking.&lt;/p&gt;
&lt;h2 id=&quot;how-to-give-ai-agent-eyes&quot;&gt;How to give AI agent &quot;eyes&quot;&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-to-give-ai-agent-eyes&quot; aria-label=&quot;Anchor link for: how-to-give-ai-agent-eyes&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;When you&#39;re running Claude Code or any other agentic coding tool, the bottleneck isn&#39;t the model&#39;s intelligence. It&#39;s the fidelity of the environment. Standard AI coding involves the agent guessing column names based on your description, or in better cases parsing a &lt;code&gt;schema.sql&lt;/code&gt; file or using a local database with seed data.&lt;/p&gt;
&lt;p&gt;None of these give the agent the one thing it actually needs: awareness of your &lt;em&gt;production&lt;/em&gt; schema. The table sizes, the indexes, the constraints, the statistics that determine whether a query index-scans in 2ms or sequential-scans for 40 seconds.&lt;/p&gt;
&lt;p&gt;The obvious fix is to give it a database connection. Let it query &lt;code&gt;pg_catalog&lt;/code&gt;, read table sizes, check existing indexes. This is how Anthropic&#39;s reference PostgreSQL MCP server worked, and that&#39;s the one Datadog Security Labs &lt;a rel=&quot;external&quot; href=&quot;https://securitylabs.datadoghq.com/articles/mcp-vulnerability-case-study-SQL-injection-in-the-postgresql-mcp-server/&quot;&gt;found a SQL injection in&lt;/a&gt;. The vulnerable npm package still gets 21,000 downloads per week. The server wrapped queries in read-only transactions, but accepted semicolons. &lt;code&gt;COMMIT; DROP SCHEMA public CASCADE;&lt;/code&gt; works because the &lt;code&gt;COMMIT&lt;/code&gt; ends the read-only transaction and everything after runs with full privileges. Read-only transactions are not a security boundary when the protocol accepts semicolons.&lt;/p&gt;
&lt;p&gt;The AI doesn&#39;t need a &lt;em&gt;connection&lt;/em&gt;. It needs the &lt;em&gt;information&lt;/em&gt;. Schema metadata is structural data. You can extract it once, save it as a JSON file, commit it to git, and hand that file to every tool in your stack.&lt;/p&gt;
&lt;div class=&quot;sidenote&quot;&gt;
&lt;a href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP&lt;/a&gt; is an open standard for connecting AI assistants to external tools and data sources. Instead of relying on training data, the agent calls structured tools exposed by an MCP server and gets deterministic answers back.
&lt;/div&gt;
&lt;p&gt;What&#39;s been missing is a standard way to wire that information into the agent&#39;s workflow. Not a one-time dump that sits in a docs folder, but a protocol that lets the agent &lt;em&gt;call tools&lt;/em&gt; and get structured answers back. That protocol is &lt;a rel=&quot;external&quot; href=&quot;https://modelcontextprotocol.io&quot;&gt;MCP&lt;/a&gt; (Model Context Protocol).&lt;/p&gt;
&lt;h2 id=&quot;what-are-postgresql-mcp-servers-actually-for&quot;&gt;What are PostgreSQL MCP servers actually for?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-are-postgresql-mcp-servers-actually-for&quot; aria-label=&quot;Anchor link for: what-are-postgresql-mcp-servers-actually-for&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;This has been bugging me. If you search for &quot;PostgreSQL MCP server&quot; today, you&#39;ll find two kinds.&lt;/p&gt;
&lt;p&gt;The first is a connection wrapper. You give it a &lt;code&gt;DATABASE_URL&lt;/code&gt;, it exposes tools like &lt;code&gt;query&lt;/code&gt; and &lt;code&gt;execute&lt;/code&gt;. This is &lt;code&gt;psql&lt;/code&gt; with extra steps. The Anthropic reference server that got the SQL injection? Connection wrapper. The dozen community forks that replaced it? Connection wrappers with slightly better input validation.&lt;/p&gt;
&lt;p&gt;The second is a managed-provider gateway. Cloud database platforms expose their platform APIs through MCP. Create a branch, provision a database, manage users. Useful if you&#39;re on that platform, but not database intelligence. Control-plane operations wearing a protocol hat.&lt;/p&gt;
&lt;p&gt;Neither kind answers the question an AI agent actually needs answered: &lt;em&gt;&quot;is this SQL going to be a problem?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A connection wrapper can run &lt;code&gt;EXPLAIN&lt;/code&gt;, if you trust it with credentials. But who tells the agent that &lt;code&gt;SET NOT NULL&lt;/code&gt; on a 4-million-row table takes an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock? Who tells it that its query defeats partition pruning? That&#39;s not a connection problem. It&#39;s a knowledge problem.&lt;/p&gt;
&lt;h2 id=&quot;dryrun&quot;&gt;dryrun&lt;a class=&quot;zola-anchor&quot; href=&quot;#dryrun&quot; aria-label=&quot;Anchor link for: dryrun&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;I didn&#39;t set out to build an AI tool. My primary project is &lt;a rel=&quot;external&quot; href=&quot;https://boringsql.com/products/regresql/&quot;&gt;RegreSQL&lt;/a&gt;, a statistics-based SQL regression testing framework. RegreSQL needs production statistics to produce realistic query plans in test environments, and dryrun&#39;s schema capture was originally just the plumbing to get those statistics into the right place.&lt;/p&gt;
&lt;p&gt;But driving adoption of a testing tool is hard. The best place to plug in is where the coding actually happens. And these days, that&#39;s increasingly AI agents. Instead of fighting it, I turned the plumbing into a proper tool.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https://boringsql.com/products/dryrun/&quot;&gt;dryrun&lt;/a&gt; is a PostgreSQL schema intelligence tool. It ships as a CLI and as an MCP server. It doesn&#39;t connect to your database at runtime. Instead, it works from a JSON snapshot of your schema, captured once, committed to git, and available to everyone.&lt;/p&gt;
&lt;h3 id=&quot;offline-first&quot;&gt;Offline first&lt;a class=&quot;zola-anchor&quot; href=&quot;#offline-first&quot; aria-label=&quot;Anchor link for: offline-first&quot;&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;One person with production access captures the schema:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dryrun&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; dump-schema&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --source&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span&gt;$PROD_URL&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --pretty --name&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;production&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; schema.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The JSON contains everything structural: tables, columns, constraints, indexes, and the rest of what you&#39;d find in &lt;code&gt;pg_catalog&lt;/code&gt;. It also captures per-table statistics from &lt;code&gt;pg_statistic&lt;/code&gt; (histograms, most common values, correlation, &lt;code&gt;n_distinct&lt;/code&gt;) and runtime counters from &lt;code&gt;pg_class&lt;/code&gt; like &lt;code&gt;reltuples&lt;/code&gt;, &lt;code&gt;seq_scan&lt;/code&gt;, and &lt;code&gt;idx_scan&lt;/code&gt;. One file, typically 200-500KB. Structural metadata, not row data.&lt;/p&gt;
&lt;div class=&quot;callout&quot;&gt;
If your schema uses &lt;code&gt;COMMENT ON&lt;/code&gt; (and it should), dryrun captures those too. A column named &lt;code&gt;status&lt;/code&gt; is ambiguous. &lt;code&gt;COMMENT ON COLUMN orders.status IS &#39;pending | completed | refunded, set by payment processor webhook&#39;&lt;/code&gt; tells the agent what the values mean, where they come from, and that it shouldn&#39;t invent new ones.
&lt;p&gt;If you don&#39;t take my word for it, check what Bruce Momjian &lt;a href=&quot;https://momjian.us/main/blogs/pgblog/2026.html#March_16_2026&quot;&gt;has to say&lt;/a&gt; about it. MCP clients can only work with schemas, object names, and constraints. Comments are the simplest way to supply the semantic context that makes AI-generated queries meaningful.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Import it and every subsequent command works offline:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dryrun&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; import schema.json&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No connection needed, no credentials on developer machines or in CI.&lt;/p&gt;
&lt;h3 id=&quot;as-an-mcp-server&quot;&gt;As an MCP server&lt;a class=&quot;zola-anchor&quot; href=&quot;#as-an-mcp-server&quot; aria-label=&quot;Anchor link for: as-an-mcp-server&quot;&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;claude&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mcp add dryrun&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; dryrun mcp-serve&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude now has 16 tools for exploring your schema, validating queries, checking migrations, and suggesting improvements, all from the offline snapshot.&lt;/p&gt;
&lt;h3 id=&quot;what-the-agent-gets&quot;&gt;What the agent gets&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-the-agent-gets&quot; aria-label=&quot;Anchor link for: what-the-agent-gets&quot;&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;dryrun exposes 16 tools through MCP. The four that matter most for the argument in this article:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;check_migration&lt;/strong&gt; is the one I find myself using most often, even outside of AI workflows. It analyzes DDL statements for lock implications, table rewrites, and version-specific behavior, and returns the safe multi-step alternative when one exists. Every migration example in this article was caught by this tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;suggest_index&lt;/strong&gt; reads column statistics from the snapshot and recommends indexes informed by your actual data distribution. Not &quot;add an index on this column&quot; but &quot;add a partial index on the 3% of rows that match your query pattern.&quot; It also uses correlation data to recommend BRIN indexes where appropriate: 3MB instead of 400MB for a B-tree on a column with near-perfect insertion order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;validate_query&lt;/strong&gt; parses SQL against the schema snapshot without executing it. It catches references to tables and columns that don&#39;t exist, detects anti-patterns, and flags issues before the query goes anywhere near the database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;lint_schema&lt;/strong&gt; runs 18 convention rules and 14 structural audit checks: naming, types, constraints, timestamps, partition health, duplicate indexes, foreign key columns missing indexes, tables without primary keys. All disableable in &lt;code&gt;dryrun.toml&lt;/code&gt;, because not every convention fits every project.&lt;/p&gt;
&lt;p&gt;The remaining tools cover schema exploration (&lt;strong&gt;list_tables&lt;/strong&gt;, &lt;strong&gt;describe_table&lt;/strong&gt;, &lt;strong&gt;search_schema&lt;/strong&gt;, &lt;strong&gt;find_related&lt;/strong&gt;), operations (&lt;strong&gt;compare_nodes&lt;/strong&gt;, &lt;strong&gt;detect&lt;/strong&gt;, &lt;strong&gt;vacuum_health&lt;/strong&gt;), execution plans (&lt;strong&gt;explain_query&lt;/strong&gt;), and schema maintenance (&lt;strong&gt;refresh_schema&lt;/strong&gt;, &lt;strong&gt;check_drift&lt;/strong&gt;). The full list is in the &lt;a rel=&quot;external&quot; href=&quot;https://github.com/boringsql/dryrun&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;try-it-yourself&quot;&gt;Try it yourself&lt;a class=&quot;zola-anchor&quot; href=&quot;#try-it-yourself&quot; aria-label=&quot;Anchor link for: try-it-yourself&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Here&#39;s a schema and seed data you can run in any local PostgreSQL:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE TABLE&lt;/span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; customers&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    id &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    name text NOT NULL&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    email &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    created_at &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz NOT NULL DEFAULT now&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE TABLE&lt;/span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; orders&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    id &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    customer_id &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint NOT NULL&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    status text NOT NULL DEFAULT&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pending&amp;#39;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    total_amount &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;numeric&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    created_at &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz NOT NULL DEFAULT now&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Seed it with data that has the distributions that matter, like skewed status values and nullable emails:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 100K customers, ~5% with NULL email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; customers (&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;/span&gt;&lt;span&gt;, email, created_at)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;customer_&amp;#39;&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;/span&gt;&lt;span&gt; i,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CASE WHEN&lt;/span&gt;&lt;span&gt; random()&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;05&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN NULL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;         ELSE&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;user&amp;#39;&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;@example.com&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;2023-01-01&amp;#39;&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;/span&gt;&lt;span&gt; (i &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39; seconds&amp;#39;&lt;/span&gt;&lt;span&gt;)::interval&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; generate_series&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;100000&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;/span&gt;&lt;span&gt; i;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 1M orders, 97% completed / 3% pending&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span&gt; orders (customer_id, &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;/span&gt;&lt;span&gt;, total_amount, created_at)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    1&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;/span&gt;&lt;span&gt; (random()&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 99999&lt;/span&gt;&lt;span&gt;)::&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CASE WHEN&lt;/span&gt;&lt;span&gt; random()&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;03&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pending&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;         ELSE&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;completed&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (random()&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 500&lt;/span&gt;&lt;span&gt;)::&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;numeric&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;2024-01-01&amp;#39;&lt;/span&gt;&lt;span&gt;::&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;/span&gt;&lt;span&gt; (i &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39; seconds&amp;#39;&lt;/span&gt;&lt;span&gt;)::interval&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; generate_series&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1000000&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;/span&gt;&lt;span&gt; i;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now capture the schema for dryrun and install it as an MCP server:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dryrun&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; dump-schema&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --source&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&lt;/span&gt;&lt;span&gt;$DATABASE_URL&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; schema.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dryrun&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; init&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dryrun&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; import schema.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;claude&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mcp add dryrun&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; dryrun mcp-serve&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Open Claude Code and try these prompts. First without the dryrun MCP server, then with it.&lt;/p&gt;
&lt;h3 id=&quot;add-an-index-to-speed-up-queries-that-filter-orders-by-status&quot;&gt;&quot;Add an index to speed up queries that filter orders by status&quot;&lt;a class=&quot;zola-anchor&quot; href=&quot;#add-an-index-to-speed-up-queries-that-filter-orders-by-status&quot; aria-label=&quot;Anchor link for: add-an-index-to-speed-up-queries-that-filter-orders-by-status&quot;&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Without dryrun, Claude sees a &lt;code&gt;text&lt;/code&gt; column and does the obvious thing:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;/span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; idx_orders_status&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;/span&gt;&lt;span&gt; orders (&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A full B-tree index on 1 million rows. It works, but 97% of those rows are &lt;code&gt;&#39;completed&#39;&lt;/code&gt; and nobody queries for completed orders.&lt;/p&gt;
&lt;p&gt;With dryrun, Claude calls &lt;strong&gt;suggest_index&lt;/strong&gt;, sees &lt;code&gt;most_common_freqs&lt;/code&gt; showing &lt;code&gt;&#39;completed&#39;&lt;/code&gt; at 97% and &lt;code&gt;&#39;pending&#39;&lt;/code&gt; at 3%, and suggests:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;/span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; idx_orders_status_pending&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;/span&gt;&lt;span&gt; orders (&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE status =&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pending&amp;#39;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full index maintains 1 million entries. The partial index maintains 30,000. Every INSERT that creates a &lt;code&gt;&#39;completed&#39;&lt;/code&gt; order no longer pays for index maintenance it&#39;ll never use.&lt;/p&gt;
&lt;h3 id=&quot;make-the-email-column-on-customers-required&quot;&gt;&quot;Make the email column on customers required&quot;&lt;a class=&quot;zola-anchor&quot; href=&quot;#make-the-email-column-on-customers-required&quot; aria-label=&quot;Anchor link for: make-the-email-column-on-customers-required&quot;&gt;&lt;/a&gt;
&lt;/h3&gt;
&lt;p&gt;Without dryrun, Claude writes a single statement and moves on:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER TABLE&lt;/span&gt;&lt;span&gt; customers &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER&lt;/span&gt;&lt;span&gt; COLUMN email &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET NOT NULL&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This fails immediately because 5% of rows have NULL emails. Claude didn&#39;t know the data existed. Even if the column had no NULLs, the statement would still take an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock and perform a full table scan to verify.&lt;/p&gt;
&lt;p&gt;With dryrun, Claude calls &lt;strong&gt;check_migration&lt;/strong&gt; and &lt;strong&gt;describe_table&lt;/strong&gt;. It sees 100,000 rows, knows the column is nullable, and knows from the statistics that NULLs exist. It generates a three-step migration:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Step 1: backfill NULLs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UPDATE&lt;/span&gt;&lt;span&gt; customers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    SET&lt;/span&gt;&lt;span&gt; email &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;unknown-&amp;#39;&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;/span&gt;&lt;span&gt; id &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;@example.com&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;/span&gt;&lt;span&gt; email &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NULL&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Step 2: add constraint without full table scan&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER TABLE&lt;/span&gt;&lt;span&gt; customers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    ADD CONSTRAINT&lt;/span&gt;&lt;span&gt; customers_email_not_null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CHECK&lt;/span&gt;&lt;span&gt; (email &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NOT NULL&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT&lt;/span&gt;&lt;span&gt; VALID;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Step 3: validate in the background with a weaker lock&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER TABLE&lt;/span&gt;&lt;span&gt; customers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    VALIDATE &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CONSTRAINT&lt;/span&gt;&lt;span&gt; customers_email_not_null;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first step handles the data. The second adds the constraint without scanning existing rows. The third validates with a weaker lock that allows concurrent reads and writes. Three statements instead of one.&lt;/p&gt;
&lt;h2 id=&quot;keeping-the-snapshot-fresh&quot;&gt;Keeping the snapshot fresh&lt;a class=&quot;zola-anchor&quot; href=&quot;#keeping-the-snapshot-fresh&quot; aria-label=&quot;Anchor link for: keeping-the-snapshot-fresh&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;dryrun is only as good as your last snapshot. If the schema changed yesterday and nobody re-dumped, the agent is working from stale data. The &lt;strong&gt;check_drift&lt;/strong&gt; tool can detect this, but only if someone runs it.&lt;/p&gt;
&lt;p&gt;The best approach is to treat schema snapshots like you treat production backups: automate them. If you&#39;re already capturing production schema dumps on a schedule, add dryrun to the same pipeline. A nightly &lt;code&gt;dump-schema&lt;/code&gt; from the primary gives you current structure and statistics. If you run read replicas with different workloads, add &lt;code&gt;--stats-only&lt;/code&gt; dumps from each node for higher-fidelity statistics across the cluster. Commit the resulting &lt;code&gt;schema.json&lt;/code&gt; to your repository so every developer and every AI agent starts the day with a current view.&lt;/p&gt;
&lt;p&gt;The snapshot also serves as a lightweight audit trail. If someone adds a column through &lt;code&gt;psql&lt;/code&gt; without a migration, the next scheduled dump captures it, and the diff in git shows exactly what changed and when.&lt;/p&gt;
&lt;h2 id=&quot;what-dryrun-is-not&quot;&gt;What dryrun is not&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-dryrun-is-not&quot; aria-label=&quot;Anchor link for: what-dryrun-is-not&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;dryrun does not execute arbitrary SQL against your database. It does not manage migrations. It does not provision infrastructure. It is not an ORM, a query builder, or a database admin panel. It&#39;s read-only and narrow by design, trustworthy enough that you&#39;d let an AI agent use it unsupervised.&lt;/p&gt;
&lt;h2 id=&quot;what-s-next&quot;&gt;What&#39;s next&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-s-next&quot; aria-label=&quot;Anchor link for: what-s-next&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;dryrun ships today at v0.5.0. The next releases are focused on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shadow EXPLAIN&lt;/strong&gt; (v0.6.0): inject production statistics into a local PostgreSQL instance so &lt;code&gt;EXPLAIN&lt;/code&gt; produces realistic plans from your laptop, without any production data. Currently &lt;strong&gt;explain_query&lt;/strong&gt; runs against whatever local database you point it at, but if that database has 10 rows in every table, the planner gives you toy plans that tell you nothing about production behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD Trust Gate&lt;/strong&gt;: a single command that takes a migration file and runs safety analysis, schema diff, lint, and index suggestions in one pass. Designed to block unsafe migrations before they merge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improved migration support&lt;/strong&gt;: deeper analysis of multi-statement migrations, version-specific advice, and integration with common migration frameworks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;getting-started&quot;&gt;Getting started&lt;a class=&quot;zola-anchor&quot; href=&quot;#getting-started&quot; aria-label=&quot;Anchor link for: getting-started&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;dryrun is MIT licensed and available on &lt;a rel=&quot;external&quot; href=&quot;https://github.com/boringsql/dryrun&quot;&gt;GitHub&lt;/a&gt;. The &lt;a rel=&quot;external&quot; href=&quot;https://github.com/boringsql/dryrun/blob/master/TUTORIAL.md&quot;&gt;tutorial&lt;/a&gt; covers installation, offline and online workflows, multi-node setups, and MCP integration. It&#39;s part of the &lt;a rel=&quot;external&quot; href=&quot;https://boringsql.com&quot;&gt;boringSQL&lt;/a&gt; suite.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>RegreSQL: Regression Testing for PostgreSQL Queries</title>
        <published>2025-11-13T22:47:00+00:00</published>
        <updated>2025-11-13T22:47:00+00:00</updated>
        
        <author>
          <name>Radim Marek</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/regresql-testing-queries/"/>
        <id>https://boringsql.com/posts/regresql-testing-queries/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/regresql-testing-queries/">&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; - &lt;em&gt;RegreSQL brings PostgreSQL&#39;s regression testing methodology to your application queries, catching both correctness bugs and performance regressions before production.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As puzzling as it might seem, the common problem with production changes is the ever-present &quot;AHA&quot; moment when things start slowing down or crashing straight away. Testing isn&#39;t easy as it is, but there&#39;s a widespread practice gap when it comes to testing SQL queries. Some might pretend to &quot;fix it&quot; by using ORMs to abstract away the problem. Others treat SQL as &quot;just glue code&quot; that doesn&#39;t deserve systematic testing. Most settle for integration tests that verify the application layer works, never actually testing whether their queries will survive the next schema change or index modification.&lt;/p&gt;
&lt;p&gt;For PostgreSQL development itself, the project has a robust regression test suite that has been preventing disasters in core development for decades. The database itself knows how to test SQL systematically - we just don&#39;t use those same techniques for our own queries. Enter &lt;a href=&quot;/products/regresql/&quot;&gt;RegreSQL&lt;/a&gt;, a tool originally created by Dimitri Fontaine for &lt;a rel=&quot;external&quot; href=&quot;https://theartofpostgresql.com&quot;&gt;&lt;em&gt;The Art of PostgreSQL&lt;/em&gt;&lt;/a&gt; book (which is excellent for understanding and mastering PostgreSQL as a database system), designed to bring the same regression testing framework to our application queries.&lt;/p&gt;
&lt;p&gt;I&#39;ve been trying to use it for some time, but due to missing features and limitations gave up several times. Until now. I decided to fork the project and spend the time needed to take it to the next level.&lt;/p&gt;
&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;a class=&quot;zola-anchor&quot; href=&quot;#introduction&quot; aria-label=&quot;Anchor link for: introduction&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://boringsql.com/products/regresql/&quot;&gt;RegreSQL&lt;/a&gt;&lt;/strong&gt; promise starts with the biggest strength and perceived weakness of SQL queries. They are just strings. And unless you use something like &lt;a rel=&quot;external&quot; href=&quot;https://sqlc.dev&quot;&gt;sqlc&lt;/a&gt; (for Go), &lt;a rel=&quot;external&quot; href=&quot;https://github.com/darioteixeira/pgocaml&quot;&gt;PG&#39;OCaml&lt;/a&gt; or Rust&#39;s &lt;a rel=&quot;external&quot; href=&quot;https://github.com/launchbadge/sqlx&quot;&gt;SQLx&lt;/a&gt; toolkit giving you compile-time checking, your queries are validated only when they are executed. Which in better case mean either usually slow-ish test suite or integration tests, in worst scenario only when deployed. ORMs are another possibility - completely abstracting away SQL (but more on that later).&lt;/p&gt;
&lt;p&gt;But even with compile-time checking, you are only checking for one class of problems: schema mismatches. What about behavior changes after schema migration or performance regressions? What about understanding whether your optimization actually made things faster or just moved the problem elsewhere?&lt;/p&gt;
&lt;p&gt;This is where RegreSQL comes in. Rather than trying to turn SQL into something else, RegreSQL embraces &quot;SQL as strings&quot; reality and applies the same testing methodology PostgreSQL itself uses: regression testing. You write (or generate - continue reading) your SQL queries, provide input data, and RegreSQL verifies that future changes don&#39;t break those expectations.&lt;/p&gt;
&lt;p&gt;The features don&#39;t stop there though - it tracks performance baselines, detects common query plan regressions (like sequential scans), and gives you framework for systematic experimentation with the schema changes and query change management.&lt;/p&gt;
&lt;h2 id=&quot;basic-regression-testing&quot;&gt;Basic regression testing&lt;a class=&quot;zola-anchor&quot; href=&quot;#basic-regression-testing&quot; aria-label=&quot;Anchor link for: basic-regression-testing&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Enough with theory. Let&#39;s jump in straight into the action and see what a sample run of RegreSQL looks like&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ regresql text&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connecting to &amp;#39;postgres://radim:password123@192.168.139.28/cdstore_test&amp;#39;… ✓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Running regression tests...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.2.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.2.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ artist_top-artists-by-album.1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-3.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-tracks_tracks-by-genre.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Results: 8 passed, 0 failed, 8 skipped (0.00s)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example based on &lt;a rel=&quot;external&quot; href=&quot;https://github.com/lerocha/chinook-database&quot;&gt;Chinook database&lt;/a&gt; (as used originally in The Art of PostgreSQL book), RegreSQL scans the current directory (or one provided by &lt;code&gt;-C /path/to/project&lt;/code&gt;) for &lt;code&gt;*.sql&lt;/code&gt; files and attempts to run all queries against the configured PostgreSQL connection.&lt;/p&gt;
&lt;p&gt;The individual files can contain either single or multiple sql queries. Like following example&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- name: top-artists-by-album&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Get the list of the N artists with the most albums&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    artist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    count&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;/span&gt;&lt;span&gt; albums&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    artist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    LEFT JOIN&lt;/span&gt;&lt;span&gt; album &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;/span&gt;&lt;span&gt; (artist_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    artist&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    albums &lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIMIT&lt;/span&gt;&lt;span&gt; :n;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax for the queries supports both positional arguments (like &lt;code&gt;$1&lt;/code&gt; known from libpq library) or (preferred) &lt;code&gt;psql&lt;/code&gt; style variable (&lt;code&gt;:varname&lt;/code&gt;). The each identified query (not file) is then executed for 0..N times, based on number of predefined plans and verified to the expected results - validating the expected data matches the one returned. The support for SQL files handling is available separately with https://github.com/boringSQL/queries (Go version only for now).&lt;/p&gt;
&lt;p&gt;This gives you what original RegreSQL tool has introduced - change your schema, refactor a query, run &lt;code&gt;regresql test&lt;/code&gt; and see immediately what broke. The test suite now has ability to catch regressions before they are committed / shipped. The current version built on top of it, giving you better console formatter instead of TAP style output, as well as jUnit, JSON and GitHub actions formatters for better integration into your CI/CD pipelines.&lt;/p&gt;
&lt;h2 id=&quot;performance-regression-testing&quot;&gt;Performance regression testing&lt;a class=&quot;zola-anchor&quot; href=&quot;#performance-regression-testing&quot; aria-label=&quot;Anchor link for: performance-regression-testing&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Basic regression testing catches correctness issues - wrong results, broken queries, schema mismatches. But there&#39;s another class of production issues it misses. Performance regressions. No matter how unbelievable it might sound but queries get deployed without appropriate indexes — which &lt;a href=&quot;/posts/why-postgresql-indexes-are-ignored/&quot;&gt;might then be ignored&lt;/a&gt; entirely — or they change over time. Simple fix - both for handwritten SQL or ORM code - can switch from milliseconds to seconds. You add index that helps one query, but tanks another. You modify conditionals and accidently force a &lt;a href=&quot;/posts/postgresql-statistics&quot;&gt;sequential scan of millions of rows&lt;/a&gt;. This is where it hurts.&lt;/p&gt;
&lt;p&gt;RegreSQL addresses this by tracking performance baselines alongside correctness. Once baselines are generated&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ regresql baseline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connecting to &amp;#39;postgres://appuser:password123@192.168.139.28/cdstore_test&amp;#39;… ✓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Creating baselines directory: regresql/baselines&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Creating directory &amp;#39;regresql/baselines&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Creating baselines for queries:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ./&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-by-artist_list-albums-by-artist.1.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-by-artist_list-albums-by-artist.2.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-tracks_list-tracks-by-albumid.1.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-tracks_list-tracks-by-albumid.2.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: artist_top-artists-by-album.1.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: genre-topn_genre-top-n.top-1.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: genre-topn_genre-top-n.top-3.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: genre-tracks_tracks-by-genre.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Baselines have been created successfully!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Baseline files are stored in: regresql/baselines&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the test command not only tests the regressions to the captured times, but also detects the common bad patterns in &lt;a href=&quot;/posts/explain-buffers/&quot;&gt;query execution plans&lt;/a&gt;. For now it provides warnings for detection of sequential scans - both on their and/or with nested loops and multiple sort operations. I believe this alone might provide a valuable insights and reduce the mishaps in production. It&#39;s also a place where further development of RegreSQL will take place.&lt;/p&gt;
&lt;p&gt;To demonstrate this, let&#39;s review the test output with the baselines.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connecting to &amp;#39;postgres://appuser:password123@192.168.139.28/cdstore_test&amp;#39;… ✓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Running regression tests...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.2.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.1.cost (22.09 &amp;lt;= 22.09 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Sequential scan detected on table &amp;#39;artist&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider adding an index if this table is large or this query is frequently executed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.2.cost (22.09 &amp;lt;= 22.09 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Sequential scan detected on table &amp;#39;artist&amp;#39;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider adding an index if this table is large or this query is frequently executed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.2.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.1.cost (8.23 &amp;lt;= 8.23 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.2.cost (8.23 &amp;lt;= 8.23 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ artist_top-artists-by-album.1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ artist_top-artists-by-album.1.cost (35.70 &amp;lt;= 35.70 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: album, artist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered/joined columns&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-1.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-3.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-1.cost (6610.59 &amp;lt;= 6610.59 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: genre, artist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered/joined columns&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sort operations detected (2 sorts)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider composite indexes for ORDER BY clauses to avoid sorting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-3.cost (6610.59 &amp;lt;= 6610.59 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: artist, genre&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered/joined columns&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sort operations detected (2 sorts)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider composite indexes for ORDER BY clauses to avoid sorting&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-tracks_tracks-by-genre.json (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-tracks_tracks-by-genre.cost (37.99 &amp;lt;= 37.99 * 110%) (0.00s)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: genre, track&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered/joined columns&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Results: 16 passed (0.00s)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, despite from not having baseline, RegreSQL is able to detect the basic bad patterns that should be addressed before queries can be considered &quot;production ready&quot;.&lt;/p&gt;
&lt;p&gt;In some cases, having the detection of sequential scans, or just tracking query costs baselines might be considered undesirable, which would lead to false positives. RegreSQL enables this to be addressed by query metadata as demonstrated below.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- name: query_name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- metadata: key1=value1, key2=value2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;/span&gt;&lt;span&gt; ...;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point RegreSQL recognizes&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;notest&lt;/code&gt; to skip the query testing altogether (not just cost tracking)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nobaseline&lt;/code&gt; to skip cost tracking&lt;/li&gt;
&lt;li&gt;&lt;code&gt;noseqscanwarn&lt;/code&gt; to keep cost tracking but disable sequential scan warnings&lt;/li&gt;
&lt;li&gt;and &lt;code&gt;difffloattolerance&lt;/code&gt; to cost failure threshold (default 10% at the moment).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- name: query_name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- regresql: notest, nobaseline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- regresql: noseqscanwarn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- regresql: difffloattolerance:0.25&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- query that can vary in cost by 20% without being considered a failure&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;/span&gt;&lt;span&gt; ...;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;orm-enters-the-room&quot;&gt;ORM enters the room&lt;a class=&quot;zola-anchor&quot; href=&quot;#orm-enters-the-room&quot; aria-label=&quot;Anchor link for: orm-enters-the-room&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;ORMs abstract away SQL, but they still generate it - much like &lt;a href=&quot;/posts/view-inlining/&quot;&gt;view inlining&lt;/a&gt; where the planner rewrites your SQL behind the scenes - and that generated SQL can have performance problems you won&#39;t catch until production. Consider this common scenario: you start with a simple SQLAlchemy query that works fine, then months later add eager loading for related data:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;orders&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    session.query(Order)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .filter(Order.user_id&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;/span&gt;&lt;span&gt; user_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .options(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        joinedload(Order.user),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        joinedload(Order.shipping_address),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        selectinload(Order.items)&lt;/span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # NEW: Load order items&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That innocent &lt;code&gt;selectinload(Order.items)&lt;/code&gt; generates a separate query - and without an index on &lt;code&gt;order_items.order_id&lt;/code&gt;, it performs a sequential scan.&lt;/p&gt;
&lt;p&gt;RegreSQL can catch this by intercepting ORM-generated SQL using SQLAlchemy&#39;s event system:&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@event.listens_for&lt;/span&gt;&lt;span&gt;(engine,&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;before_cursor_execute&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; capture_sql&lt;/span&gt;&lt;span&gt;(conn, cursor, statement,&lt;/span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;/span&gt;&lt;span&gt;args):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    captured_queries.append(statement)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run your ORM code, capture the SQL, save it as a .sql file, and test it with RegreSQL. The performance baseline testing will flag the missing index before it hits production. This is currently experimental, but ORM integration is a key area for RegreSQL&#39;s future development.&lt;/p&gt;
&lt;h2 id=&quot;test-data-management&quot;&gt;Test Data Management&lt;a class=&quot;zola-anchor&quot; href=&quot;#test-data-management&quot; aria-label=&quot;Anchor link for: test-data-management&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Up until now we have covered how RegreSQL verifies query correctness and tracks performance regressions. But there&#39;s a critical prerequisite we&#39;ve only skimmed through.  Every regression test needs consistent, reproducible data. Change the data, change their cardinality, and your expected results become meaningless. Your performance  baselines drift. Your tests become flaky.&lt;/p&gt;
&lt;p&gt;Traditional approach to create test data might involve&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database dumps&lt;/strong&gt; become unmanageable - 500MB files you can&#39;t review, can&#39;t understand, that break with every schema migration, and whose data becomes stale as production evolves. Which version of the dump are your tests even using?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQL scripts&lt;/strong&gt; might be better than dumps, but still imperative and hard to maintain. You end up with INSERT statements scattered across multiple files, managing foreign keys manually, and debugging constraint violations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Factories in application code&lt;/strong&gt; might work great for integration tests, but we&#39;re testing SQL directly. Do you really want to maintain parallel data generation in your application language just for SQL tests?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared test database&lt;/strong&gt; is the synonym for classic &quot;works on my machine&quot; problem. State leaks between tests. Parallel execution becomes impossible. Debugging is a nightmare.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What we need is something that&#39;s declarative (what data, not how to insert it), reproducible (similar data every time), composable (build complex scenarios from simple pieces), and scalable (from 10 rows to 100,000).&lt;/p&gt;
&lt;p&gt;This is where next improvement in RegreSQL&#39;s fixture system comes in. Think of it as infrastructure-as-code for your test data. You describe the data you need in YAML files, and RegreSQL handles the rest - dependencies, cleanup, foreign keys, and even realistic data generation at scale.&lt;/p&gt;
&lt;p&gt;RegreSQL&#39;s fixture system lets you define test data in YAML files stored in &lt;code&gt;regresql/fixtures/&lt;/code&gt;. Here&#39;s a simple example&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixture&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; basic_users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; a handful of test users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  cleanup&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; rollback&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      rows&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; alice@example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Alice Anderson&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          created_at&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2024-01-15&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bob@example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Bob Builder&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          created_at&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2024-02-20&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use this fixture in your tests, reference it in the query&#39;s plan file (&lt;code&gt;regresql/plans/get-user.yaml&lt;/code&gt;) you can just reference the fixture&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixtures&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; basic_users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  &amp;quot;1&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; alice@example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  &amp;quot;2&amp;quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bob@example.com&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when you run &lt;code&gt;regresql test&lt;/code&gt;, the fixture is automatically loaded before the query executes, and cleaned up afterward. No manual setup scripts, no state leakage between tests. But it does not stop with static fixtures. When you want to test queries against realistic volumes you can use range of &lt;strong&gt;data generators&lt;/strong&gt; including&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sequences, random integer, decimal, string, uuid, email and name generators&lt;/li&gt;
&lt;li&gt;date_between for generating random timestamps within a range&lt;/li&gt;
&lt;li&gt;foreign key references to be able to reuse data from other table&#39;s fixtures&lt;/li&gt;
&lt;li&gt;range to select value from predefined sources&lt;/li&gt;
&lt;li&gt;Go template support&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; fixture&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; realistic_orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  generate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; customers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      count&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      columns&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sequence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; email&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          domain&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shop.example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; name&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; full&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        created_at&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; date_between&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2023-01-01&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          end&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2024-12-31&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      count&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      columns&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sequence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        customer_id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          min&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          max&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1000&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        amount&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; decimal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          min&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10.00&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          max&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 999.99&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          precision&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        order_date&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; date_between&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2023-01-01&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          end&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2024-12-31&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generates 1,000 customers and 5,000 orders with realistic-looking data - names, emails, dates, and amounts that feel production-like.&lt;/p&gt;
&lt;p&gt;The fixtures are also &lt;strong&gt;stackable&lt;/strong&gt; and can be build on top of each other. For example if you need to make sure users fixtures are created before orders fixtures, just declare the dependency (the already planned improvement is to include the support automatic foreign-key detection to avoid ID hard-coding). RegreSQL loads fixtures in dependency order and handles cleanup in reverse.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixture&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders_with_shipping&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  depends_on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; basic_users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      rows&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 101&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          user_id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # References Alice from basic_users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          total&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 99.99&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shipped&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Should the available options for fixtures (manual data or data generators) not be enough, you always have options to use good old SQL based data generation.&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixture&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mixed_setup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Combine SQL with YAML and generated data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  cleanup&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; rollback&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # SQL executes first (either as file or inline)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  sql&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; file&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sql/setup_schema.sql&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; inline&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;INSERT INTO config (key, value) VALUES (&amp;#39;version&amp;#39;, &amp;#39;1.0&amp;#39;);&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # followed YAML data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; users&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      rows&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          email&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; admin@example.com&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # and finally generated data&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  generate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      count&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      columns&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sequence&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        user_id&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          generator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          min&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          max&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RegreSQL provides commands to inspect and validate your fixtures&lt;/p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot; &gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # List all available fixtures&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures list&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Show fixture details and dependencies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures show realistic_orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Validate fixture definitions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures validate&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Show dependency graph&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures deps&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Apply fixture manually (for debugging)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;/span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures apply basic_users&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fixture system has been design to transforms test data from a maintenance burden into a documented, version-controlled process. Your YAML files become the single source of truth for what data your tests need, making it easy to understand test scenarios and maintain test data as the application evolves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(EDIT 2025-11-20)&lt;/strong&gt; The dynamically generated fixtures on each tests will break the correctness testing of
RegreSQL. You have to use fixtures according to your use&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed generated fixtures re-used between tests&lt;/li&gt;
&lt;li&gt;Use RegreSQL to test fixtures before/after migration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;m working on next release of RegreSQL to address DX of fixtures usage.&lt;/p&gt;
&lt;h2 id=&quot;regresql-future&quot;&gt;RegreSQL future&lt;a class=&quot;zola-anchor&quot; href=&quot;#regresql-future&quot; aria-label=&quot;Anchor link for: regresql-future&quot;&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Introducing a new open source project is an ambitious goal, and &lt;a rel=&quot;external&quot; href=&quot;https://boringsql.com/products/regresql/&quot;&gt;RegreSQL&lt;/a&gt; is just starting up. Despite the fork being in works for almost 2 years. In coming weeks and months I plan further improvements, as well as better documentation and more tutorials. The project is maintained as part of my &lt;strong&gt;boringSQL&lt;/strong&gt; brand, where it&#39;s vital component (together with pgTap) for building &lt;a rel=&quot;external&quot; href=&quot;https://labs.boringsql.com&quot;&gt;SQL Labs&lt;/a&gt; which (as I sincerely hope) will provide a foundation for its further development.&lt;/p&gt;
&lt;p&gt;At the same time &lt;strong&gt;RegreSQL&lt;/strong&gt; is an attempt to give back to welcoming PostgreSQL community, make developer user experience slightly better if possible and (just maybe) provide one more argument against the case that SQL queries are not testable.&lt;/p&gt;
&lt;p&gt;RegreSQL is available at &lt;a href=&quot;/products/regresql/&quot;&gt;boringsql.com/products/regresql&lt;/a&gt; - feel free to open issue, or drop me email about the project at &lt;a href=&quot;mailto:radim@boringsql.com&quot;&gt;radim@boringsql.com&lt;/a&gt; or connect on &lt;a rel=&quot;external&quot; href=&quot;https://www.linkedin.com/in/1radim/&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Are SQL &amp; Databases Boring? Absolutely - and That&#39;s a Good Thing!</title>
        <published>2024-04-21T00:00:00+00:00</published>
        <updated>2024-04-21T00:00:00+00:00</updated>
        
        <author>
          <name>Radim Marek</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/boring-sql-and-databases/"/>
        <id>https://boringsql.com/posts/boring-sql-and-databases/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/boring-sql-and-databases/">&lt;p&gt;Twenty years ago, I would have laughed if you had told me I&#39;d be promoting databases as the technology to turn to. Back then, databases were just a &#39;dummy&#39; storage for me—a necessary evil, a sentiment shared by many developers. At that point, I felt so strongly about it that I was a trainer for Hibernate, a tool which helped me keep those pesky databases at arm&#39;s length.&lt;/p&gt;
&lt;p&gt;Fast forward, and here I am, starting &lt;strong&gt;boringSQL&lt;/strong&gt;, a site dedicated to helping demystify the depths of SQL and databases in general. Whether you&#39;ve just started or want to gain expert knowledge. What changed, you might ask? Nothing actually. Well, a few things did. The frameworks changed. I moved from the Java world to Rails, jumped to Python and then to Go, worked with countless frontend paradigms, and oversaw the implementation of a number of technologies. The same experiments were tried again and again. Only one part of the tech stood the test of time. Yes, it&#39;s the database. And out of many, one in particular—PostgreSQL. And while alternatives promised, and still promise, that the grass would be greener—I can honestly say that whatever revolution comes in the technology landscape, databases will hold the same place in the next twenty years.&lt;/p&gt;
&lt;p&gt;This is not the first &lt;strong&gt;boring&lt;/strong&gt; site, and rest assured, the concept of celebrating the mundane in technology is far from new. The tech landscape is paved with many &quot;boring&quot; things, from frameworks to methodologies to complete stacks. Businesses depend on these technologies to run their operations day in, day out. And while it might be fun to try new things every month, it&#39;s not the headline-driven development that is the way forward.&lt;/p&gt;
&lt;p&gt;Hence, if you ask—is SQL boring? Are databases boring? I would also say YES. They are among the finalists of the least popular conversation starters. But it&#39;s the quiet power of reliability that has a healing effect in a world where technology changes at the speed of light.&lt;/p&gt;
</content>
        
    </entry>
</feed>
