Skip to content

sqlb-survey: the whole-database adoption report (#112) - #113

Merged
jryannel merged 1 commit into
mainfrom
claude/sqlb-maintenance-tradeoffs-lbf0s9
Aug 2, 2026
Merged

sqlb-survey: the whole-database adoption report (#112)#113
jryannel merged 1 commit into
mainfrom
claude/sqlb-maintenance-tradeoffs-lbf0s9

Conversation

@jryannel

@jryannel jryannel commented Aug 2, 2026

Copy link
Copy Markdown
Owner

An adoption currently discovers what the DSL cannot express one table at a time, because the only way to ask is to write a throwaway program (#112). This is that program, kept.

Why three phases

The flat skip list is not the question an adopter has.

  • Phase A introspects the whole schema — what a drift gate over the whole database would see.
  • Phase B introspects every table alone. This is what makes the result triageable: introspect reports per construct, but the gate is per registry, so one unmodelable table takes its whole module out (A composite PRIMARY KEY has no declaration, so every natural-key table pays for a surrogate it does not need #109). Per-table isolation names the blocked tables instead of leaving them mixed into a list of skips.
  • Phase C renders the modelled registry into a scratch database and re-introspects, separating a construct that survives import but not the round trip from one that never imported.

Result on a 68-table production schema

clean — imports with nothing dropped 57
partial — imports, constructs dropped 11
refused 0
skipped constructs / distinct reasons 15 / 6
DDL statements to rebuild 329
apply failures 0
residual after round trip 8

The 8 residuals are 4 CHECK constraints dropped and re-added. Every one is on a varchar(n) column; the three text columns carrying the same kind of IN list round-trip clean. Postgres normalises the list two different ways depending on the column type:

-- varchar column, as stored
CHECK (status::text = ANY ((ARRAY['backlog'::varchar, …])::text[]))
-- what the re-emitted DDL normalises to
CHECK (status::text = ANY (ARRAY[('backlog'::varchar)::text, …]))

Semantically identical, textually different. Worth knowing whether that belongs here or in the adopter's schema — for the subject above it was cheaper to move the columns to text, which ADR-0017 wanted anyway.

One finding that is the tool's own setup

Diff renders no CREATE EXTENSION, so bootstrapping into a bare database fails once per table with function uuid_generate_v4() does not exist rather than once with the missing extension named. With uuid-ossp and vector pre-created, the same 329 statements applied cleanly. That is recorded in the command's doc comment; whether Diff should emit extensions is a separate question and not decided here.

Notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01Scg4WyZ3hV6ZqLjQPL2ckK


Generated by Claude Code

An adoption discovers what the DSL cannot express one table at a time,
because the only way to ask is to write a throwaway program (#112). This
is that program, kept.

Three phases, because the flat skip list is not the question an adopter
has. Phase A introspects the whole schema, which is what a drift gate
would see. Phase B introspects every table ALONE, which is what makes the
result triageable: introspect reports per construct, but the gate is per
registry, so one unmodelable table takes its whole module out (#109).
Per-table isolation names the blocked tables instead of leaving them
mixed into a list of skips. Phase C renders the modelled registry into a
scratch database and re-introspects, so a construct that survives import
but not the round trip is separated from one that never imported.

Run against a 68-table production schema: 57 tables clean, 11 partial, 0
refused, 15 skipped constructs over 6 reasons, and 8 residual changes
after the round trip -- 4 CHECK constraints dropped and re-added, all of
them on varchar columns, none on the text columns beside them.

One finding is the tool's own setup and belongs in the doc comment rather
than in a report: Diff renders no CREATE EXTENSION, so bootstrapping into
a bare database fails once per table with a missing uuid_generate_v4
rather than once with the missing extension named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Scg4WyZ3hV6ZqLjQPL2ckK
@jryannel
jryannel merged commit 89ed317 into main Aug 2, 2026
3 checks passed
@jryannel
jryannel deleted the claude/sqlb-maintenance-tradeoffs-lbf0s9 branch August 2, 2026 10:12
jryannel added a commit that referenced this pull request Aug 2, 2026
…t a schema (#117)

A flat verdict list under-reports a modular monolith. The gate is per
registry and a modular monolith has one registry per module (ADR-0015),
so a table that cannot be modelled does not take out "the schema" -- it
takes out its module. Whether eleven blocked tables mean one app or six
is the number that decides how much of a port is blocked, and it is not
visible in a list sorted by table name.

    sqlb-survey -modules billing,catalog src dst

regroups the per-table verdict the run already computes, and marks a
module blocked when any of its tables is partial or refused. Tables
matching no prefix are counted together and listed, since that set is
either the shared core or a prefix missing from the flag, and the reader
has to be able to tell which.

Prefixes are supplied rather than inferred. Guessing them from table
names splits hotel_rooms from hotels, and a wrong split reads as a real
result. Longest prefix wins so a module named user does not claim the
tables of user_billing.

On the 68-table schema from #113 the eleven partial tables fall across
five of seven module groups, which is the shape the flat list hid: the
blockers are diffuse, so no single module unblocks by fixing one thing.


Claude-Session: https://claude.ai/code/session_01Scg4WyZ3hV6ZqLjQPL2ckK

Co-authored-by: Claude <noreply@anthropic.com>
jryannel added a commit that referenced this pull request Aug 2, 2026
… needs no patch (#118)

The exclusion list was the literal string goose_db_version. Pointing the
survey at a project on golang-migrate or atlas meant editing the source,
and forgetting to meant its bookkeeping table arrived as an unmodelable
table -- a blocker that is not one.

-exclude replaces the list. The default now names five runners so the
common cases need no flag at all.

Naming five runners does not mean excluding five tables, and getting that
wrong cost a wrong number here first: introspect reports a name in
Exclude it cannot find, deliberately, because a typo would otherwise
silently shrink what a gate checks. Right for a gate, wrong for a default
list of which four entries are absent from any given project -- they
arrived as four extra skipped constructs, and the run said 19 where the
schema has 15. So the list is narrowed to what the database actually
holds before it reaches introspect, and the header prints the narrowed
list rather than the wish.

Narrowing needs the unfiltered table list, which is where the second bug
was: listTables took the skip list as a parameter for the first time, nil
binds as SQL NULL, and `tablename <> ALL(NULL)` is NULL rather than true,
so the discovery call matched no rows and the survey reported zero
tables.

Output on the schema from #113 is unchanged: 68 tables, 15 skipped
constructs, 57 clean / 11 partial / 0 refused, fixpoint residual 8.


Claude-Session: https://claude.ai/code/session_01Scg4WyZ3hV6ZqLjQPL2ckK

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants