Skip to content

Fix SQLite/Postgres schema column parity checks - #2346

Merged
Wirasm merged 4 commits into
devfrom
archon/task-archon-fix-github-issue-experimental-1785419694882
Jul 31, 2026
Merged

Fix SQLite/Postgres schema column parity checks#2346
Wirasm merged 4 commits into
devfrom
archon/task-archon-fix-github-issue-experimental-1785419694882

Conversation

@Wirasm

@Wirasm Wirasm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Problem: The SQLite/Postgres parity test checked only table names, allowing missing shared SQLite columns to pass CI.
  • Why it matters: SQLite is the default installation path; silent schema drift can cause runtime failures for SQLite users.
  • What changed: Extend the SQLite adapter parity suite to parse PostgreSQL CREATE TABLE and ALTER TABLE ... ADD COLUMN declarations, compare expected column names against fresh SQLite PRAGMA table_info results, and retain the workflow-run index assertion.
  • What did not change (scope boundary): No runtime schema behavior, migrations, type/constraint parity, or SQLite-only extra-column policy changed. The known remote_agent_codebases.allow_env_keys PostgreSQL-only residue remains an explicit, narrowly scoped fix(db): codebases.allow_env_keys exists in Postgres and not SQLite — dead column, live drift #2318 exception.

UX Journey

Before

Developer                 CI parity test                 SQLite user
─────────                 ──────────────                 ───────────
adds PG column ─────────▶ checks table names only
                          passes despite missing column ─▶ encounters runtime schema error

After

Developer                 CI parity test                 SQLite user
─────────                 ──────────────                 ───────────
adds PG column ─────────▶ [parses expected column names]
                          [compares them with SQLite PRAGMA]
                          fails on unallowlisted drift ──▶ receives schema with CI-protected parity

Architecture Diagram

Before

migrations/000_combined.sql ──▶ bundled-schema.ts ──▶ sqlite.test.ts [table names only]
sqlite.ts fresh schema ─────────────────────────────▶ sqlite.test.ts

After

migrations/000_combined.sql ──▶ bundled-schema.ts ──▶ [~ sqlite.test.ts: table + column names]
sqlite.ts fresh schema ─────────────────────────────▶ [~ sqlite.test.ts: PRAGMA column comparison]

Connection inventory (list every module-to-module edge, mark changes):

From To Status Notes
migrations/000_combined.sql packages/core/src/db/bundled-schema.ts unchanged Provides PostgreSQL schema SQL.
packages/core/src/db/bundled-schema.ts packages/core/src/db/adapters/sqlite.test.ts modified Test now derives expected shared column names as well as table names.
packages/core/src/db/adapters/sqlite.ts packages/core/src/db/adapters/sqlite.test.ts modified Fresh SQLite schema is now inspected through PRAGMA table_info for column parity.

Label Snapshot

  • Risk: risk: low
  • Size: size: M
  • Scope: tests
  • Module: core:sqlite-adapter

Change Metadata

  • Change type: bug
  • Primary scope: core

Linked Issue

Validation Evidence (required)

Commands and result summary:

bun test packages/core/src/db/adapters/sqlite.test.ts  # 18 passed
bun run type-check                                    # passed
bun run lint                                          # passed, 0 warnings
bun run format:check                                  # passed
bun run test                                          # passed per validation artifact
bun run build                                         # passed
  • Evidence provided (test/log/trace/screenshot): The focused test includes parser anti-vacuity checks and a failure-sensitivity experiment that reported a deliberately excluded expected column; the temporary change was reverted.
  • If any command is intentionally skipped, explain why: None. An earlier full-validation attempt was affected by pre-existing, unstaged .archon/ workflow edits; the validation run regenerated their corresponding bundled artifact and subsequently recorded all commands as passing. Those unrelated files are intentionally excluded from this PR.

Security Impact (required)

  • New permissions/capabilities? (No)
  • New external network calls? (No)
  • Secrets/tokens handling changed? (No)
  • File system access scope changed? (No)
  • If any Yes, describe risk and mitigation: Not applicable.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Database migration needed? (No)
  • If yes, exact upgrade steps: Not applicable.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios: Fresh SQLite schema exposes every expected shared PostgreSQL column; upgrade-only PostgreSQL column declarations are included; the parent_run_id index remains covered.
  • Edge cases checked: Parser sanity checks cover remote_agent_user_ai_prefs.default_model; PostgreSQL auth tables stay excluded; only remote_agent_codebases.allow_env_keys is excepted for fix(db): codebases.allow_env_keys exists in Postgres and not SQLite — dead column, live drift #2318.
  • What was not verified: Cross-dialect types, constraints, and SQLite-only extra columns are deliberately out of scope.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Core SQLite adapter schema-parity test only.
  • Potential unintended effects: Future intentional PostgreSQL-only columns will fail CI until explicitly justified and narrowly allowlisted.
  • Guardrails/monitoring for early detection: Deterministic per-table missing-column failures and parser anti-vacuity assertions make drift actionable in CI.

Rollback Plan (required)

  • Fast rollback command/path: Revert commit e929b01a from this PR branch.
  • Feature flags or config toggles (if any): None.
  • Observable failure symptoms: Schema-parity test fails for a shared PostgreSQL column absent from fresh SQLite.

Risks and Mitigations

  • Risk: SQL parsing could mistake a constraint for a column or miss a declaration form.
    • Mitigation: Filter constraint keywords, parse both creation and upgrade forms, and assert known table/column parser output.

Summary by CodeRabbit

  • Tests
    • Improved database schema validation to provide more reliable comparisons across supported database systems.
    • Added coverage for complex SQL definitions, including comments, quoted values, nested expressions, and safely separated declarations.
    • Expanded checks to detect missing or extra tables and columns, while ensuring schema comparison coverage and configuration lists remain up to date.

The schema parity suite compared table names only, allowing missing SQLite columns to ship undetected.

Changes:
- Parse Postgres CREATE TABLE and ALTER TABLE column declarations
- Compare every shared Postgres column against fresh SQLite schema columns
- Keep the #2318 allow_env_keys exception exact and retain parent_run_id index coverage

Fixes #2319
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 845ddc8d-88d4-447d-b553-8e7607000015

📥 Commits

Reviewing files that changed from the base of the PR and between 3044829 and f41260e.

📒 Files selected for processing (1)
  • packages/core/src/db/adapters/sqlite.test.ts

📝 Walkthrough

Walkthrough

This PR extends schema parity tests to compare columns, not just table names, between the PostgreSQL migration and SQLite schema. New parsing helpers handle SQL comments, quotes, and nested parentheses to extract table and column definitions reliably. Tests validate bidirectional column parity and allowlist accuracy.

Changes

Schema Parity Column Checks

Layer / File(s) Summary
SQL parsing helpers
packages/core/src/db/adapters/sqlite.test.ts
New helper functions remove comments, preserve quoted text, extract balanced table bodies, and split column declarations at top-level commas. Unbalanced parentheses cause a throw instead of truncated data.
Parity test setup and PostgreSQL parsing
packages/core/src/db/adapters/sqlite.test.ts
Test setup tracks PostgreSQL-only and SQLite-only column exceptions, enforces a minimum parsed-column count, and extracts table and column data from the PostgreSQL migration and a fresh SQLite schema. Table parsing ignores comments. The table filter uses the table variable consistently.
Bidirectional column tests and allowlist validation
packages/core/src/db/adapters/sqlite.test.ts
New tests compare columns in both directions, check that comparisons are non-empty, report drift, and confirm column allowlists match actual schema differences. The existing fresh-schema index regression test remains in the suite.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

  • coleam00/Archon#2033: Modifies the same test file to enforce schema parity; this PR substantially expands the parser and column-level checks introduced there.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch archon/task-archon-fix-github-issue-experimental-1785419694882

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Wirasm

Wirasm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Consolidated Review: PR #2346

Date: 2026-07-30T14:23:00Z
Agents: code-review, error-handling, test-coverage, comment-quality, docs-impact
Total Findings: 2


Executive Summary

This is a focused, test-only schema-parity change with no production, configuration, or user-facing documentation impact. The PostgreSQL-to-SQLite comparison is deterministic, keeps the known allow_env_keys exception precisely column-scoped, and preserves the separate parent_run_id index coverage. One medium issue prevents the anti-vacuity check from independently proving that ALTER TABLE ... ADD COLUMN declarations are parsed. A low-priority rationale comment also needs updating because it now describes only one of two intentional exception mechanisms.

Three requested reviewer artifacts were not available at synthesis time (error-handling, test-coverage, and docs-impact), so their zero counts mean no artifact was received—not a completed clean review.

Overall Verdict: REQUEST_CHANGES

Auto-fix Candidates: 0 CRITICAL + HIGH issues can be auto-fixed
Manual Review Needed: 2 MEDIUM + LOW issues require decision


Statistics

Agent CRITICAL HIGH MEDIUM LOW Total
Code Review 0 0 1 0 1
Error Handling* 0 0 0 0 0
Test Coverage* 0 0 0 0 0
Comment Quality 0 0 0 1 1
Docs Impact* 0 0 0 0 0
Total 0 0 1 1 2

* Artifact unavailable at synthesis time.


CRITICAL Issues (Must Fix)

None identified in the available artifacts.


HIGH Issues (Should Fix)

None identified in the available artifacts.


MEDIUM Issues (Options for User)

Issue 1: Upgrade-parser anti-vacuity assertion is not independent

Source Agent: code-review
Location: packages/core/src/db/adapters/sqlite.test.ts:445
Category: Test regression coverage

Problem:

The asserted default_model column appears in both its table's CREATE TABLE declaration and the idempotent ALTER TABLE ... ADD COLUMN IF NOT EXISTS statement. If the new ADD COLUMN parser stopped matching upgrade declarations, the assertion would still pass. The suite therefore does not independently prove the upgrade-parser path contributes a column.

Options:

Option Approach Effort Risk if Skipped
Fix Now Assert remote_agent_users.role, which is declared only in the Postgres upgrade ALTER block. LOW A regression that drops all parsed upgrade columns can remain undetected.
Create Issue Defer the assertion correction to a separate PR. LOW The parity test ships with a known coverage gap.
Skip Accept the current indirect coverage. NONE Future parser refactors can silently omit upgrade-only columns.

Recommendation: Fix now. It is a minimal test-only correction that directly validates the behavior this PR adds.

const codebaseColumns = postgresColumns.get('remote_agent_codebases');
const userColumns = postgresColumns.get('remote_agent_users');

// Anti-vacuity checks cover a CREATE declaration and a genuinely ALTER-only declaration.
expect(codebaseColumns?.has('allow_env_keys')).toBe(true);
expect(userColumns?.has('role')).toBe(true);

LOW Issues (For Consideration)

Issue Location Agent Suggestion
Parity rationale still claims there is only one allowlist exception packages/core/src/db/adapters/sqlite.test.ts:375 comment-quality Update the JSDoc to distinguish the Better Auth table-prefix exclusion from the exact remote_agent_codebases.allow_env_keys exception tracked by #2318.

Recommended wording:

 * Better Auth's remote_agent_auth_* tables are intentionally Postgres-only
 * (web auth never runs on SQLite  see migrateColumns() and CLAUDE.md), so
 * the table-parity check excludes that prefix. The separate, exact
 * `remote_agent_codebases.allow_env_keys` column exception is tracked by
 * #2318; keep it column-specific. A genuinely new Postgres-only table must
 * be added to the table allowlist with a justifying comment.

Positive Observations

  • The remote_agent_codebases.allow_env_keys exclusion is qualified to that exact column, avoiding a broad table- or prefix-level exception.
  • The parser explicitly excludes SQL constraints, avoiding false column detections.
  • The fresh-SQLite comparison is one-way in the intended PostgreSQL-to-SQLite direction and sorts missing columns deterministically.
  • The separate parent_run_id index assertion remains in place.
  • The implementation stays test-local, typed, and avoids introducing a premature reusable schema abstraction.
  • Focused SQLite adapter validation passed with 18 tests and no failures.
  • No production or user-facing documentation change is needed for this scoped test-only PR.

Suggested Follow-up Issues

Issue Title Priority Related Finding
None recommended; fix the upgrade-parser assertion in this PR. MEDIUM issue #1

Next Steps

  1. Fix the medium anti-vacuity assertion using a genuinely ALTER-only column such as remote_agent_users.role.
  2. Update the stale parity-rationale comment to document both intentional exception mechanisms.
  3. Re-run the focused SQLite adapter test after the change.
  4. Obtain the three missing reviewer artifacts before relying on this as a complete five-agent review.

Agent Artifacts

Agent Artifact Findings
Code Review code-review-findings.md 1
Error Handling error-handling-findings.md (unavailable)
Test Coverage test-coverage-findings.md (unavailable)
Comment Quality comment-quality-findings.md 1
Docs Impact docs-impact-findings.md (unavailable)

Metadata

  • Synthesized: 2026-07-30T14:23:00Z
  • Artifact: /Users/rasmus/.archon/workspaces/coleam00/Archon/artifacts/runs/96345c1f6d1776d32afbde63a3573285/review/consolidated-review.md

Fixed:\n- cover ALTER-only PostgreSQL migration columns in the schema parity test\n- document the table and column parity exceptions\n\nTests added:\n- strengthened existing SQLite schema parity coverage\n\nSkipped:\n- none
@Wirasm

Wirasm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

⚡ Self-Fix Report (Aggressive)

Status: COMPLETE
Pushed: ✅ Changes pushed to archon/task-archon-fix-github-issue-experimental-1785419694882
Philosophy: Fix everything unless clearly a new concern


Fixes Applied (2 total)

Severity Count
🔴 CRITICAL 0
🟠 HIGH 0
🟡 MEDIUM 1
🟢 LOW 1
View all fixes
  • Upgrade-parser anti-vacuity assertion is not independent (packages/core/src/db/adapters/sqlite.test.ts:448) — asserts ALTER-only remote_agent_users.role.
  • Parity rationale still claims there is only one allowlist exception (packages/core/src/db/adapters/sqlite.test.ts:377) — documents both narrow exception mechanisms.

Tests Added

(none — strengthened the existing SQLite schema-parity test)


Skipped (0)

(none — all findings addressed)


Suggested Follow-up Issues

(none)


Validation

✅ Type check | ✅ Lint | ✅ Tests


Self-fix by Archon · aggressive mode · fixes pushed to archon/task-archon-fix-github-issue-experimental-1785419694882

@Wirasm

Wirasm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

pr: 2346
title: "Fix SQLite/Postgres schema column parity checks"
author: "Wirasm"
reviewed: 2026-07-30T17:45:00Z
recommendation: request-changes

PR Review: #2346 — Fix SQLite/Postgres schema column parity checks

Branch: archon/task-archon-fix-github-issue-experimental-1785419694882dev
Files Changed: 1 (+69/-24) — packages/core/src/db/adapters/sqlite.test.ts
State: DRAFT · MERGEABLE · CI green · 0 commits behind dev


Summary

This is a guard, so I judged it empirically rather than stylistically: I checked out the branch in a throwaway worktree, injected drift in both directions and by both declaration routes, and recorded whether the test went red.

It works in the direction it was built for. Injected column drift is caught via the CREATE TABLE route and via the ALTER TABLE … ADD COLUMN route, with precise table.column failure output. The live allow_env_keys divergence is genuinely detected — it passes on dev only because of one explicit, documented, column-scoped allowlist entry.

But the parser has two ways to go quiet, and one of them takes the pre-existing table-parity check down with it. I confirmed a case where four real columns become invisible and the suite still reports 3 pass / 0 fail, and a case where an entire table disappears from the table-level check that this file already had. Those are the failure mode a parity guard exists to prevent — passing while blind — so they block.


Execution log (what I actually ran)

Isolated worktree at /Users/rasmus/Projects/cole/Archon-review-2346 (detached, bun install --frozen-lockfile, removed afterwards). The primary repo was never modified. All edits below were reverted; the tree was verified clean (git status --porcelain empty) before validation.

# Experiment Result
0 Baseline, unmodified PR branch 3 pass / 0 fail
1 Added drift_probe_create TEXT to remote_agent_workflow_events CREATE body FAIL+ "remote_agent_workflow_events.drift_probe_create"
2 Appended ALTER TABLE remote_agent_messages ADD COLUMN IF NOT EXISTS drift_probe_alter TEXT; FAIL+ "remote_agent_messages.drift_probe_alter"
3 Emptied POSTGRES_ONLY_COLUMNS (live-drift acid test) FAIL+ "remote_agent_codebases.allow_env_keys"
4 Added sqlite_only_probe TEXT to remote_agent_messages in sqlite.ts (reverse direction) 3 pass / 0 fail — not detected
5 Added in-body SQL comment -- see remote_agent_workflow_runs(id); parsed columns 7 → 3; 3 pass / 0 fail — not detected
6 Wrapped column definition across two lines inside a CREATE body phantom column references produced
7 ALTER TABLE … ADD COLUMN probe_no_ine TEXT; (no IF NOT EXISTS) silently not parsed
8 CREATE body terminated )\n; following table vanished from the parsed map entirely

Answers to the three questions posed:


Issues Found

Critical

None.

Important

I1 — Table discovery is now coupled to CREATE-body parsing; a body-parse failure silently drops tables from the table-parity check too

packages/core/src/db/adapters/sqlite.test.ts:431

The old postgresArchonTables() matched CREATE TABLE <name> alone — body-independent. The new expected list derives from postgresArchonColumns().keys() (sqlite.test.ts:431), and a table is only registered there if the full \(([\s\S]*?)\); body match succeeded.

Confirmed (experiment #8): given two tables where the first closes with )\n;, the parser returned only remote_agent_alpharemote_agent_beta was gone from the map entirely, along with a phantom create column absorbed from the swallowed text.

Failure scenario: this is the PR-2033 drift class — a whole table present in Postgres and missing from SQLite. That is the drift the table-parity test in this file was originally written for. A single body-parse hiccup anywhere in 000_combined.sql re-opens it, and the expect(expected.length).toBeGreaterThan(10) floor at sqlite.test.ts:437 does not catch losing one or two tables out of nineteen.

Fix: keep the original body-independent table-name regex as the source of the table list, and use it to seed the column map:

const tableNameRe = /CREATE TABLE(?:\s+IF NOT EXISTS)?\s+"?([a-z0-9_]+)"?/gi;
for (const m of sql.matchAll(tableNameRe)) {
  const t = m[1].toLowerCase();
  if (t.startsWith('remote_agent_')) columnsByTable.set(t, new Set());
}
// …then fill columns from the body + ALTER passes as today.

Table discovery then cannot regress below what it caught before this PR.

I2 — ); anywhere inside a CREATE body silently truncates the column list, and the guard stays green

packages/core/src/db/adapters/sqlite.test.ts:393

/…\s*\(([\s\S]*?)\);/gi is non-greedy, so the body ends at the first ); — not at the matching close paren.

Confirmed (experiment #5): inserting the ordinary comment -- see remote_agent_workflow_runs(id); into the remote_agent_workflow_events body cut the parsed column set from 7 to 3. step_index, step_name, data and created_at became invisible to the parity check, and the suite still reported 3 pass / 0 fail.

Failure scenario: someone documents an FK in a comment (-- FK to remote_agent_users(id);) — 000_combined.sql already carries in-body comments at lines 179–194, so this is ordinary house style, not a contrived input. Every column below that comment loses parity coverage, permanently and silently. The guard reports success while blind, which is strictly worse than the pre-PR state where nobody believed there was column coverage.

Fix: strip -- comments before matching, and split the body on top-level commas with paren-depth tracking rather than on \n. Depth tracking also fixes I3 and S3 as a side effect. Pair it with the count floor in I3 so a future collapse fails loudly instead of quietly.

I3 — Anti-vacuity guards prove two columns survive, not that the parse is intact

packages/core/src/db/adapters/sqlite.test.ts:450-452

The two assertions are well chosen, and the self-fix that introduced users.role is correct — I verified it is genuinely ALTER-only (the remote_agent_users CREATE body declares 5 columns; the parser reports 6, so the upgrade path is demonstrably contributing).

But they are point checks. Experiment #5 dropped four columns from a different table with both assertions still true.

Fix: add a total-column floor alongside them. The parser currently yields 136 columns across the 15 non-auth tables, so a conservative floor is cheap and catches partial collapse:

const totalColumns = [...postgresColumns]
  .filter(([t]) => !t.startsWith(POSTGRES_ONLY_PREFIX))
  .reduce((n, [, cols]) => n + cols.size, 0);
expect(totalColumns).toBeGreaterThan(120); // 136 today

Suggestions

S1 — Reverse direction is uncovered, and there is a live instance of it in the repo right now

The check is one-way (PG → SQLite). Experiment #4 confirmed a SQLite-only column passes unnoticed.

Scoping to one direction was defensible — all three drifts named in #2319 point the same way. But while probing I found a fourth, pointing the other way: remote_agent_isolation_environments.updated_at exists in sqlite.ts and not in 000_combined.sql. It is dead (no reader in packages/core/src/db/isolation-environments.ts), which makes it an exact mirror of the allow_env_keys situation.

That matters for cost: I enumerated the residue, and that column is the only reverse-direction difference in the whole schema. There are no SQLite-only tables. So the reverse check is one allowlist entry away from being enforceable today.

Worth weighing: SQLite-only drift is the works-in-dev, breaks-in-production direction — local dev defaults to SQLite, the VPS runs Postgres. Recommend adding the reverse pass in this PR, or filing it explicitly rather than leaving it implied by "out of scope".

S2 — ADD COLUMN without IF NOT EXISTS is invisible to the parser

packages/core/src/db/adapters/sqlite.test.ts:411 requires the literal ADD COLUMN IF NOT EXISTS. Experiment #7 confirmed a plain ADD COLUMN is not parsed.

Low probability — all 20 existing ALTERs use IF NOT EXISTS, since the migration must be idempotent. But a contributor who forgets it breaks the idempotency convention and silently loses parity coverage for that column, with nothing flagging either. Making the clause optional (ADD COLUMN(?:\s+IF NOT EXISTS)?) costs nothing and closes the hole.

S3 — Wrapped column definitions produce a phantom references column

Experiment #6: a column definition wrapped across two lines yields a bogus references entry, because the line-split heuristic reads the continuation line as a new declaration and references is not in TABLE_CONSTRAINTS (sqlite.test.ts:386).

Lower severity than I2 — it produces a false failure, which is loud and fail-safe rather than silent. Same root cause; the depth-tracking fix in I2 resolves it. Note the wrapped style is exactly how every ALTER … ADD COLUMN … REFERENCES in this migration is already written, so it is a natural style for someone to carry into a CREATE body.

S4 — Make the POSTGRES_ONLY_COLUMNS allowlist self-expiring

packages/core/src/db/adapters/sqlite.test.ts:385

The entry is correct today. But when #2318 is resolved by adding the column to SQLite, the entry becomes dead and nothing forces its removal — and a stale allowlist entry is precisely the "place to bury future drift" the issue warns about. Assert that each exempted column is still genuinely absent:

// A resolved exception must be deleted, not left to rot.
for (const q of POSTGRES_ONLY_COLUMNS) {
  const [t, c] = q.split('.');
  expect(new Set(raw_pragma(currentDbPath, t)).has(c)).toBe(false);
}

S5 — Nothing in 000_combined.sql points at the guard

Placement in sqlite.test.ts is correct — the existing table-parity test lives in the same describe block at sqlite.test.ts:363, so this is the right home and the property is discoverable from the file that already owns it. But the person most likely to trip the check is someone editing the migration, and that file says nothing about the guard. A one-line comment near the ALTER block ("columns here are parity-checked against sqlite.ts in sqlite.test.ts") would shorten the trip from red CI to understanding.


Assessment: the allowlist entry for the live drift

Established definitively (experiment #3): the check works; the allowlist is what makes it pass. Emptying POSTGRES_ONLY_COLUMNS produces exactly one failure, remote_agent_codebases.allow_env_keys.

I judge this a satisfied exception rather than a silenced check, for four reasons: it is scoped to one exact table.column rather than a table or prefix; it carries an inline justification and an issue reference (sqlite.test.ts:383-385); it follows the precedent already set by the remote_agent_auth_ table exclusion; and it is the option #2318 itself sanctions ("the new check needs an allowlist entry").

The reservation worth stating: it is uncomfortable for a guard's first act to exempt the only live instance of the class it guards. But the alternative — propagating a dead column into a second dialect purely for symmetry — is worse, and the decision belongs to #2318, which is open. Recommend keeping it, adding S4 so the entry cannot outlive its reason, and noting on #2318 that closing it means deleting this line.

Assessment: names-only was the right recommendation

The issue's call to compare names only, not types, holds up. Every known drift — the three in #2319 plus the reverse-direction isolation_environments.updated_at I found — is a presence drift, not a mistyped one. And a cross-dialect type map would be asserting something SQLite does not enforce anyway: SQLite uses type affinity, so a declared TEXT against a Postgres UUID constrains nothing at runtime. The map would be pure maintenance cost for no additional caught bugs. No change needed here.


Validation Results

Check Status Details
Type Check PASS all 9 packages exit 0
Lint PASS 0 warnings (--max-warnings 0)
Format PASS prettier clean
Tests (@archon/core, full) PASS all batches exit 0
Targeted (sqlite.test.ts) PASS 18 tests, 3 in the parity block
CI PASS ubuntu, windows, docker-build all SUCCESS

Strengths

  • The core mechanism works. Both declaration routes are genuinely handled — verified independently, not just asserted.
  • Failure output is precise and actionable: sorted table.column entries, so a red CI run names the exact drift.
  • The allowlist is column-scoped and issue-referenced, not a broad table or prefix exemption.
  • The self-review's users.role anti-vacuity fix is substantively correct, not cosmetic — it is genuinely the ALTER-only path.
  • The constraint-keyword filter works on today's SQL: I dumped all 19 parsed tables and their columns and found no false positives.
  • Removing the bespoke parent_run_id column assertion was right — the generic check subsumes it — while correctly keeping the index assertion, which the column check does not cover.
  • Scope discipline: one test file, no production changes, no premature shared schema abstraction.

Recommendation

REQUEST CHANGES (posted as a comment — PR is a draft).

I1 and I2 are the blockers, and they are the same class of problem the PR exists to fix: a check that reports success while unable to see. Both are fixed by one change — parse bodies with paren-depth tracking after stripping comments, and keep table discovery on a body-independent regex — plus the count floor in I3 so the next collapse is loud.

S1 is a judgment call worth making explicitly rather than inheriting: the reverse direction costs one allowlist entry today, and there is already a real instance sitting in the repo.


Reviewed by Claude · empirical verification in an isolated worktree, since removed

Follow-up to the column-level parity check. Three ways it could pass while
seeing nothing:

1. Table discovery had become a by-product of column-body parsing (the list
   came from postgresArchonColumns().keys()), so a table only registered if
   its full `(...);` body matched. A `)\n;` terminator made the *following*
   table vanish from the map entirely — re-opening the PR #2033 drift class
   (whole table in the migration, missing from sqlite.ts) that the original
   table-parity test exists to catch. Table discovery is back on a
   body-independent `CREATE TABLE <name>` regex.

2. The non-greedy body match ended at the first `);`, including one inside a
   comment. `migrations/000_combined.sql` already writes `);` in prose
   (lines 447, 467, 469), so this is house style, not a contrived input:
   an ordinary `-- see remote_agent_workflow_runs(id);` cut
   remote_agent_workflow_events from 7 parsed columns to 3 with the suite
   still green. Comments are now stripped first, and bodies are read with
   paren-depth tracking and split on top-level commas, so nested parens in
   REFERENCES / CHECK / DEFAULT clauses cannot end a body early.

3. The anti-vacuity pair only proved two named columns survived parsing —
   both stayed true while four others silently disappeared. Added a floor on
   the number of columns actually compared (136 today, verified by execution;
   the truncating parser scores 132).

Also adds the reverse pass. isolation_environments.updated_at exists in
sqlite.ts and not in the migration, and is the only reverse-direction
difference in the schema with no SQLite-only tables, so the check costs one
allowlist entry. Reverse drift is the works-locally / breaks-on-the-Postgres-VPS
direction, which is worth a guard.

Both allowlists are now self-expiring: each entry must still describe real
drift, so fixing #2318 fails the suite until the entry is deleted rather than
quietly leaving a live column unprotected.

Comparison stays names-only — SQLite type affinity means a declared TEXT
against a Postgres UUID constrains nothing, so a type map would add cost
without catching a bug.
@Wirasm

Wirasm commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

pr: 2346
title: 'Fix SQLite/Postgres schema column parity checks'
author: 'Wirasm'
reviewed: 2026-07-30
scope: 'fix commits 18a1a1d + 91c9d34 only'
recommendation: request-changes

PR Review (pass 2): #2346 — schema-parity parser fix

Scope: the two fix commits 18a1a1da and 91c9d34b, treated as unreviewed code.
Verified in a throwaway detached worktree (Archon-rereview2346, since removed); the
primary checkout was never modified.


Verdict

NEEDS FIXES — one Important issue, narrow and ~3 lines to resolve.

Every claim in the fix commits (B1, B2, B3, reverse pass, self-expiring allowlists)
is empirically correct. All five requested injections behave exactly as specified,
and the pre-existing table-parity test is un-regressed. The parser is genuinely
sound, including the quote-awareness that was not asked for.

The one problem is that the new MIN_NON_AUTH_COLUMNS guard is placed before the
assertion it is meant to protect, so in one realistic scenario it hides the
actionable drift list — the same class of "green/loud but blind" failure this PR
exists to eliminate.


Item 1 — Injection results (observed, not expected)

Baseline before any injection: 20 pass / 0 fail.

# Injection Observed
a Table body terminated )\n;, bogus column on the following table REDremote_agent_workflow_node_sessions.zz_injected_after_terminator reported
b Comment containing ); inside remote_agent_workflow_events body GREEN (correct) — and provably non-lossy: compared count stayed exactly 136
c Postgres-only column added via a CREATE TABLE body REDremote_agent_user_provider_keys.zz_create_only
d Postgres-only column added via trailing ALTER TABLE … ADD COLUMN REDremote_agent_users.zz_alter_only
e SQLite-only column (reverse pass) REDremote_agent_workflow_node_sessions.zz_sqlite_only

Supplementary, for the strong form of B1 — a table whose body is genuinely
unparseable (unbalanced paren) and absent from sqlite.ts:

  • every non-auth Postgres table is created by the SQLite schemaRED, naming
    remote_agent_zz_unparseable. Table discovery is body-independent as claimed.
  • The column parser did not degrade silently: it threw
    Unbalanced parentheses in schema SQL at index 9221. Fail-fast, per CLAUDE.md.

Injection (b) deserves a note on method: "green" alone would not prove the comment
was harmless. Re-probing the compared count with the comment in place returned
136, identical to baseline, so no column was dropped. The exact-valued floor is
what makes this verifiable — a happy side effect of the choice criticised below.


Item 2 — Is 136 right, and is it a floor or a trap?

The value is correct: the parser compares exactly 136 non-auth columns today
(verified by forcing the assertion to report the actual count). Per-table audit
confirms all 15 non-auth tables contribute non-zero counts summing to 136, with
remote_agent_workflow_events back at its full 7 — the truncation victim, restored.

But it has zero headroom, and it is asserted first. Two demonstrated consequences:

  1. Any legitimate column removal fails a test named "every non-auth Postgres column
    exists in a fresh SQLite schema"
    with the message Expected: >= 136 / Received: 135.
    Nothing is missing; the test name misdescribes the failure.
  2. Worse — it can mask real drift. With two legitimate PG-side removals plus one
    genuine new drift (zz_real_drift), the floor fired first and the drift list was
    never printed. Observed output was only Expected: >= 136 / Received: 135. The
    actionable finding was invisible in that run.

On the value itself: I recommend keeping 136, not adding headroom. The collapse
this guards against cost only 4 columns (7→3), so a floor of e.g. 130 would miss it.
Exact-count is the right sensitivity. The problem is ordering and messaging, not the
number.

Recommended fix (Important)

packages/core/src/db/adapters/sqlite.test.ts — move the anti-vacuity guard into its
own test so both signals surface independently under truthful names:

test('every non-auth Postgres column exists in a fresh SQLite schema', async () => {
  // …
  expect(missing.sort()).toEqual([]);   // drift first — the actionable list
});

test('the migration parser still sees the whole schema (anti-vacuity)', async () => {
  db = createTestDb();
  const postgresColumns = postgresArchonColumns();
  let compared = 0;
  for (const table of postgresArchonTables()) {
    if (table.startsWith(POSTGRES_ONLY_PREFIX)) continue;
    compared += (postgresColumns.get(table) ?? new Set()).size;
  }
  if (compared < MIN_NON_AUTH_COLUMNS) {
    throw new Error(
      `Parsed only ${compared} non-auth columns (floor ${MIN_NON_AUTH_COLUMNS}). ` +
        `If columns were intentionally removed, lower MIN_NON_AUTH_COLUMNS; ` +
        `otherwise the migration parser is silently dropping columns.`
    );
  }
});

The minimum acceptable alternative is simply swapping the two expect lines so
missing is asserted first — that alone removes the masking. The explicit throw
is what makes the failure self-explaining to the next person.


Item 3 — Quote-awareness

Extracted the four helpers verbatim and exercised them directly. 12/12 semantic
checks pass; 8/8 malformed inputs terminate in 0 ms
(no hang, no mis-index, no crash).

Confirmed correct:

  • Doubled single quotes ('it''s fine') — literal does not end early, and a --
    inside such a literal is not treated as a comment.
  • Doubled double quotes inside a quoted identifier ("we""ird").
  • Quoted identifiers containing , or ) neither split a declaration nor skew depth.
  • String literal containing ) does not close a table body.
  • REFERENCES t(id) / CHECK (b = 1) / DEFAULT NOW() all survive.
  • Apostrophe in a prose comment (-- don't do this) does not open a literal — the
    comment branch consumes it first.
  • Line comment terminated by EOF; block comment containing );.

Malformed-input behaviour is safe in every case: unterminated literal clamps to
sql.length; unterminated block comment consumes to EOF; unbalanced parens throws.

Two theoretical residues, neither reachable from the current file (verified: the
migration has no /* */ blocks, no $$ dollar-quoting, and no line with
an odd apostrophe count outside comments):

  • A stray apostrophe outside a literal would swallow the remainder as a string. Both
    downstream paths are loud (throw, or the floor), never silent.
  • splitTopLevelCommas lets depth go negative on an unmatched ). Unreachable —
    its only input comes from readBalancedParens, which is balanced by construction.

No change requested here. The quote-awareness was not asked for and is correct.


Item 4 — SQLITE_ONLY_COLUMNS self-expiry

Works, in both directions.

Both messages state the reason, so the required action is obvious. The allowlists are
also minimal and accurate: the per-table audit found exactly two asymmetries across
all 15 non-auth tables, and both are the allowlisted ones.

Note the #2318 case also trips the floor as collateral (Item 2, consequence 1) —
the test's own comment promises this scenario fails "until the allowlist entry is
deleted"
, and it does, but with one extra confusing failure alongside the clear one.


Item 5 — Did the fix break the pre-existing table-parity test?

No. A well-formed Postgres-only table absent from sqlite.ts produces RED on
every non-auth Postgres table is created by the SQLite schema, naming
remote_agent_zz_missing_table. The B1 regression is genuinely repaired, and the
unparseable-body variant above shows the guard now holds even in the case that
originally broke it.


Item 6 — Anything neither review has examined

Nothing further of substance. Checked and clear: stripSqlComments output indices
stay consistent with the readBalancedParens calls that consume them; the reverse
test's exclusion of remote_agent_auth_* is correct by construction (auth tables
never exist in SQLite) though only implicitly so; TABLE_CONSTRAINTS covers the only
two top-level constraint keywords the migration actually uses (PRIMARY, UNIQUE);
over-parsing is self-catching because a phantom name would surface in missing; and
all 34 quoted identifiers in the file live in excluded Better Auth tables.

Suggestions only:

  • S1 — The aggregate floor could in principle be netted out by a per-table collapse
    coinciding with growth elsewhere. A per-table minimum map would close it; probably
    not worth the maintenance. Note only.
  • S2 — No $$ dollar-quoting support. None exists today, and adding one would fail
    loud rather than silent. Note only.
  • S318a1a1da's commit message contains literal \n escape sequences instead of
    newlines (template bug in whatever generated it).
  • S4 — The PR body's validation evidence says 18 passed; the suite is now 20 tests.
    Worth refreshing before undrafting.

Validation

Check Status Details
Type check PASS all 6 packages exit 0
Lint PASS 0 warnings
Format PASS prettier clean on the changed file
Tests (file) PASS 20 pass / 0 fail
Tests (@archon/core) PASS full batched suite exits 0
CI on PR head PASS ubuntu, windows, docker-build

Strengths

  • Body-independent table discovery genuinely restores the PR fix(db): create remote_agent_user_ai_prefs in the SQLite schema #2033 guard — verified
    against an unparseable body, the hardest form of the case.
  • readBalancedParens throws instead of returning a truncated body. Exactly the
    fail-fast posture CLAUDE.md asks for, and the reason the silent-truncation class
    cannot recur.
  • Self-expiring allowlists work in all four directions tested, with messages that
    name the reason rather than just the column.
  • 91c9d34b caught and fixed a flaw in 18a1a1da: the anti-vacuity probe had been
    pointed at an allowlisted column (allow_env_keys), which would have coupled the
    probe to the exception. Moving it to default_cwd was the right call.
  • Commit message and inline documentation are unusually good — they explain the
    failure mode, cite the real line numbers, and state what was deliberately left out
    (type comparison) and why.

Recommendation

NEEDS FIXES — resolve the MIN_NON_AUTH_COLUMNS ordering/messaging issue
(Item 2). Everything else verified correct; no other changes requested.

Reviewed by Claude — second pass, scoped to 18a1a1da+91c9d34b

The MIN_NON_AUTH_COLUMNS floor was asserted before the missing-column check in
the same test, so it could mask the drift it exists to protect. Demonstrated:
two legitimate column removals plus one genuine drift made the floor fire first
and the drift list never printed.

Drift is now asserted first. The floor follows as an explicit throw rather than
an expect(), because a bare "Expected: >= 136 / Received: 135" under a test
named "every non-auth Postgres column exists in a fresh SQLite schema" reads as
drift when it is either a parser regression or a legitimate removal -- the
message now says which to check and how to respond.

136 kept with no headroom, deliberately: the collapse this guards cost only 4
columns (workflow_events 7 -> 3), so a lower floor would miss it.

Verified: 20/20 on the clean tree; with 2 removals + 1 injected drift the run
now names remote_agent_workflow_events.zz_real_drift, which the previous
ordering swallowed.

Raised in second-pass review of #2346.
@Wirasm
Wirasm marked this pull request as ready for review July 31, 2026 08:07
@Wirasm
Wirasm merged commit 513d22c into dev Jul 31, 2026
4 checks passed
@Wirasm
Wirasm deleted the archon/task-archon-fix-github-issue-experimental-1785419694882 branch July 31, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test(db): schema parity check compares table names only — three column drifts have shipped through it

1 participant