# Database Query Verification

When the experiment loop optimizes a database query — raw SQL rewrite, CTE restructuring, index addition, or ORM query refactor — the standard verification steps (compile + unit tests) are insufficient. Mocked tests verify JS logic around the query, not that the query returns correct data.

This guide defines verification tiers for DB query optimizations, ordered cheapest → most thorough. **Tier 1 is mandatory for any raw SQL change. Tiers 2-3 are strongly recommended when infrastructure allows.**

## Tier 1: EXPLAIN Plan Comparison (mandatory for raw SQL changes)

Compare query plans before and after the optimization. This catches structural mistakes (wrong JOINs, missing WHERE clauses, accidentally broader/narrower result sets).

**Use `EXPLAIN` (not `EXPLAIN ANALYZE`) by default** — `EXPLAIN ANALYZE` actually executes the query, which is unsafe for mutations and may affect production data.

```typescript
// Safe: plan only, no execution
const beforePlan = await prisma.$queryRaw`EXPLAIN ${beforeQuery}`;
const afterPlan = await prisma.$queryRaw`EXPLAIN ${afterQuery}`;
```

**If you need actual row counts** (not just estimates), wrap in a transaction and roll back:

```typescript
await prisma.$transaction(async (tx) => {
  const plan = await tx.$queryRaw`EXPLAIN ANALYZE ${query}`;
  console.log(plan);
  throw new Error('ROLLBACK'); // force rollback — nothing persists
});
```

This executes the query to get real row counts and timing, but rolls back all effects.

### What to check in the plan

| Check | Before → After | Problem if different |
|-------|---------------|---------------------|
| Estimated rows | Should be ~same | Query may return different result set |
| Scan type | Seq Scan → Index Scan = good | If reversed, the "optimization" made it worse |
| Join type/order | Should match or improve | Different join order can change semantics with outer joins |
| Filter conditions | All original conditions preserved | Missing WHERE clause = data leak |
| Sort node | Should match or disappear (index covers it) | Missing sort = different ordering behavior |

### When to escalate to Tier 2

- EXPLAIN shows different estimated row counts between before and after
- The query involves outer joins, UNIONs, or subqueries (plan comparison alone can miss semantic drift)
- The rewrite changes the query structure significantly (e.g., 3 UNIONs → single WHERE...OR)

## Tier 2: Result Diffing Against Dev/Staging

Run both the original and optimized queries against a real database and compare results.

### Prerequisites

- Access to a dev or staging database with representative data
- Both queries can be executed as SELECT (or wrapped to be read-only)

### Procedure

```typescript
// Run both queries against the same database state
const beforeResults = await prisma.$queryRaw`${originalQuery}`;
const afterResults = await prisma.$queryRaw`${optimizedQuery}`;

// Compare row counts
console.log(`Before: ${beforeResults.length} rows`);
console.log(`After: ${afterResults.length} rows`);

// Compare actual data (sort both for deterministic comparison)
const sortKey = (row) => JSON.stringify(row);
const beforeSorted = [...beforeResults].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
const afterSorted = [...afterResults].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));

const match = JSON.stringify(beforeSorted) === JSON.stringify(afterSorted);
console.log(`Results match: ${match}`);

if (!match) {
  // Find specific differences
  const beforeIds = new Set(beforeResults.map(r => r.id));
  const afterIds = new Set(afterResults.map(r => r.id));
  const missing = [...beforeIds].filter(id => !afterIds.has(id));
  const extra = [...afterIds].filter(id => !beforeIds.has(id));
  console.log(`Missing from optimized: ${missing.length}`);
  console.log(`Extra in optimized: ${extra.length}`);
}
```

### What to check

- **Row count**: Must be identical. Any difference = correctness regression → DISCARD.
- **Row content**: Sample 10-20 rows and verify field values match.
- **Ordering**: If the original query has ORDER BY, verify the optimized result preserves it.
- **NULL handling**: CTE rewrites and WHERE...OR consolidations often change NULL behavior. Check that rows with NULL values in filter columns appear/disappear consistently.
- **Aggregates**: If the query aggregates (COUNT, SUM, AVG), verify the numbers match exactly.

### If no dev/staging database is available

- Check if the project has a Docker Compose or similar local DB setup
- Check for seed scripts in the codebase (`prisma/seed.ts`, `scripts/seed.*`)
- If none exists, escalate to Tier 3 (generate a seeded test)
- If no DB access at all, **flag the optimization as unverified** in results.tsv and HANDOFF.md

## Tier 3: Generated Integration Test

Create a test with a seeded database that exercises both the original and optimized queries, then assert equivalence. This persists as a regression guard for future changes.

### Procedure

1. **Create a seed dataset** that covers the query's edge cases:
   - Rows that match all filter conditions (should appear in results)
   - Rows that match some but not all conditions (should NOT appear)
   - Rows with NULL values in filter/join columns
   - Enough data to exercise joins and aggregations

2. **Write the test**:

```typescript
// __tests__/integration/dashboard-query.test.ts
import { prisma } from '../lib/prisma';

beforeAll(async () => {
  // Seed test data
  await prisma.user.createMany({ data: testUsers });
  await prisma.repository.createMany({ data: testRepos });
  await prisma.optimizationEvent.createMany({ data: testEvents });
});

afterAll(async () => {
  // Clean up in reverse dependency order
  await prisma.optimizationEvent.deleteMany({});
  await prisma.repository.deleteMany({});
  await prisma.user.deleteMany({});
  await prisma.$disconnect();
});

test('dashboard query returns correct results', async () => {
  const result = await getDashboardStats(testUserId);

  expect(result.totalOptimizations).toBe(expectedCount);
  expect(result.repositories).toHaveLength(expectedRepoCount);
  // Assert on specific known values from seed data
  expect(result.repositories[0].name).toBe('test-repo');
});

test('dashboard query handles edge cases', async () => {
  // User with no repositories
  const empty = await getDashboardStats(emptyUserId);
  expect(empty.totalOptimizations).toBe(0);

  // User with NULL username (if applicable)
  const nullUser = await getDashboardStats(nullUsernameUserId);
  expect(nullUser).toBeDefined();
});
```

3. **Run the test** against both the original and optimized implementations to confirm identical results.

### When to generate integration tests

- The optimization rewrites raw SQL (CTEs, UNIONs, complex JOINs)
- The query is a critical path (dashboard, billing, auth)
- No existing integration tests cover the query
- The project already has integration test infrastructure (test DB, seed scripts)

## Experiment Loop Integration

When the experiment loop detects a DB query optimization, extend the standard verification:

### After step 6 (Implement) — detect DB changes

Check if the diff modifies:
- `$queryRaw` / `$executeRaw` calls
- Complex Prisma queries (nested where, multiple joins, aggregations)
- SQL files or query builder patterns
- Schema changes (`schema.prisma`, migrations)

### After step 8 (Verify output equivalence) — escalate verification

If a DB change is detected:

1. **Always run Tier 1** (EXPLAIN comparison) for raw SQL changes
2. **Run Tier 2** (result diffing) if dev/staging DB is accessible
3. **Generate Tier 3** (integration test) if the query is on a critical path and no integration test exists

### Keep/Discard additions

```
DB query change detected?
+-- YES → EXPLAIN plans compared?
|   +-- NO → Run Tier 1 before proceeding
|   +-- YES → Row estimates match?
|       +-- NO → DISCARD (correctness risk)
|       +-- YES → Scan type improved or maintained?
|           +-- Improved → Continue to standard keep/discard
|           +-- Degraded → DISCARD (performance regression at DB level)
+-- NO → Standard verification (compile + tests + guard)
```

### Recording in results.tsv

For DB query optimizations, add verification tier to the notes column:

```
experiment 5 | action.ts:getDashboardStats | KEEP | 340ms→45ms | db-verified:tier1+tier2
experiment 6 | user.service.ts:findUser | KEEP | findFirst→findUnique | db-verified:tier1 (type-safe)
experiment 7 | reports.ts:generateReport | DISCARD | CTE rewrite | db-unverified (no staging DB)
```

## Safe Prisma Query Patterns (verification shortcuts)

Some Prisma query changes are inherently safe due to type system constraints:

| Change | Safe? | Why |
|--------|-------|-----|
| `findFirst` → `findUnique` (on unique field) | Yes | Prisma types enforce the unique constraint — if it compiles, it hits the same row |
| Adding `select` to narrow fields | Yes | Subset of same data — can't return wrong rows |
| Adding `take`/`skip` for pagination | Mostly | Same data, just paginated — verify boundary behavior |
| Rewriting `include` → separate `findMany` | Verify | Logically equivalent but sensitive to N+1 and ordering |
| Raw SQL rewrite of Prisma query | **Tier 1+ required** | No type safety — must verify plan and results |
| CTE restructuring | **Tier 2+ recommended** | Complex semantic changes — plan comparison alone may miss edge cases |
| UNION consolidation | **Tier 2+ recommended** | NULL handling and deduplication behavior can change |
