AWS re:Invent happy hour: Dec 1, 6 PM PST at MGM Grand RSVP

The API tests passed. The database didn't.

The API tests passed. The database didn't.

(Updated: )

The node-mariadb demo deliberately simulates two releases of a products API. Its second version adds database work that can be easy to miss when you inspect only HTTP responses.

The report shown below exposes a per-product audit query, startup DDL, and changes to SQL statement shapes. Treat the displayed counts and timings as one demo recording, then repeat the comparison with controlled data for your own service.

The lesson: if you only test APIs, you only test APIs. Compare the SQL workload between releases and the regressions show up before production does.

Why HTTP green lies

API tests answer “did we return 200 with the right JSON?” They do not answer “did we quietly 10× our database chatter?”

That gap gets wider with AI-assisted development. The 2025 DORA report found AI adoption correlates with higher delivery throughput and higher instability when control systems lag behind. More code, faster. Same blind spot on the database side.

The classic failure mode is the N+1. One list endpoint. One innocent loop. A SELECT COUNT(*) per row. HTTP response looks fine. Query count explodes.

Schema drift is quieter. A migration runs at boot. New columns appear in INSERTs. An audit table starts collecting writes. None of that shows up in a contract test.

You need a diff of what the app actually executed against the database, not what you wish it executed.

Record two releases, compare the workload

We built a demo for this on purpose. The node-mariadb app is a Node.js CRUD API backed by MariaDB. Flip APP_VERSION=v1 or v2 and you get two simulated releases with realistic database sins baked in.

Clone it:

git clone https://github.com/speedscale/demo.git
cd demo/node-mariadb
make certs && make infra && npm install

Record v1 and v2 with proxymock intercepting both HTTP and MariaDB traffic. The mysql:// prefix on the port map matters , without it you capture opaque TCP, not parsed SQL:

proxymock record \
  --app-port 3001 \
  --map 13306=mysql://localhost:3306 \
  --out proxymock/recorded-v1 \
  --svc-name node-mariadb-v1

Start the app (no TLS to the mapped port while recording), drive traffic, stop, repeat with APP_VERSION=v2 and recorded-v2. The full step-by-step is in the proxymock guide.

Then open proxymock web and run Compare SQL between runs:

The Automations menu showing Compare SQL between runs under Discovery

Pick your baseline and candidate:

The compare picker with recorded-v1 as baseline and recorded-v2 as candidate

No database credentials. No ORM hooks. Just the wire traffic you already recorded.

What the report catches

proxymock normalizes literals to ?, fingerprints each statement, and buckets the diff into new, removed, changed, and persistent queries. The headline row tells you whether v2 added statements, queries, DB time, or tables before you scroll.

SQL compare verdict strip and What changed digest for v1 vs v2

On our demo run, v2 surfaced:

  • An N+1 , a new per-product audit COUNT with an execution rate that dwarfed everything else
  • Shape changes , SELECT * became an explicit column list; INSERT grew a discount column and picked up RETURNING
  • Schema migrations , DDL captured from the recording, not guessed from migrations folders
  • DB time by table , audit_log appearing from zero and eating most of the delta

The Changed panel shows inline token diffs so you can see exactly what shifted:

Changed statements with inline diffs for SELECT and INSERT shape changes

That is the regression story HTTP tests miss. Same endpoint. Different query shape. More columns. More round trips.

Performance panels that mean something

Switch to the Performance chapter and the drift is rate-normalized per 1000 queries. That adjusts for total SQL volume, but different endpoint mixes or data cardinality can still make a comparison misleading. That is how the N+1 stands out even when you recorded slightly different traffic volumes.

Execution-count drift and DB time by table showing audit_log at +60 ms

p95 latency regressions only flag when both sides have enough timed samples. Low-sample deltas grey out on purpose. I have been burned by chasing noise from a ten-request recording.

The Tables & schema chapter adds the structural layer: DDL events, an operation × table workload matrix, and column drift inferred from query text. No live schema introspection required.

Schema changes, table workload matrix, and column drift on products

For CI, the same diff is available headless:

proxymock automation sql-compare \
  --baseline proxymock/recorded-v1 \
  --candidate proxymock/recorded-v2 \
  --output json

Exit code 2 means a side had no SQL , check protocol routing, TLS decoding, traffic selection, and whether the application actually issued queries.

The enterprise angle

My customers ask the same two questions: do we need another agent in the path, and does our traffic leave the laptop?

This workflow needs neither for the comparison itself. You record during a normal test run. The diff runs locally against RRPair files. That is a shorter security conversation than “install our database proxy in every cluster.”

It also complements what you already have. OpenTelemetry tells you aggregate latency moved. SQL compare tells you which statements moved and which tables paid for it. I still want traces for always-on dashboards. I want this diff before I merge a release that touched data access.

Try it on your next release

If you have two builds and a way to drive the same traffic through both, you have enough to diff the database workload. Start with the node-mariadb demo if you want deliberate regressions to practice on.

The API tests passed on my v2. They would have passed in production right up until MariaDB got expensive. The SQL diff caught it on a laptop on Friday. That is the test I wish I had run on Thursday.

Frequently asked questions

What changes can SQL comparison catch when HTTP responses match?

It can reveal new or removed statement shapes, execution-count drift, captured DDL, and timing changes in recorded SQL traffic. That can expose an N+1 candidate even when JSON responses remain unchanged. Query-text inference and captured timings are evidence to investigate; they are not a complete live schema inspection or a query-plan explanation.

How do I control test data between the two recordings?

Start both versions from comparable disposable database state, run the same input mix, and control startup migrations, caches, and background work. Capture the database connection in both runs. Rates normalized per 1,000 SQL queries help compare volume, but cannot compensate for different endpoint mixes or data cardinality. Repeat timing comparisons with adequate samples.

Does this replace testing against the real database?

No. Comparing saved files does not execute queries or validate constraints, isolation, persistence, or an upgrade’s query plans. Record against a real test database when those properties matter, and use database replay against a disposable candidate engine for upgrade checks. Application tests with mocked query responses establish a different, narrower result.

Sources: SQL comparison workflow and limits, Database replay and regression testing.

Stop writing API mocks by hand

proxymock records real traffic from your running app and replays it as mocks for HTTP, gRPC, Postgres, Kafka, and more. Install locally, activate once, and start recording.