Files
pad/cmd
xarmian e89c8c8ab6 fix(cli): two ways the preflight and its remedy disagreed with the migration (BUG-2810)
Codex round 9, both confirmed against the code rather than reasoned about.

**PAD_DATABASE_URL was treated as proof of a PostgreSQL deployment**, so the
flow this unit prescribes broke on itself. cmd_server.go opens PostgreSQL only
when PAD_DB_DRIVER=postgres; PAD_DATABASE_URL is ALSO migrate-to-pg's target,
and its default. An operator who follows the preflight — refused, told to run
`pad db repair-nul`, with the target URL still exported in their shell — got
"This deployment is PostgreSQL ... Nothing to scan or repair" and exit 0. The
remedy the refusal names did nothing, which is the failure mode this unit has
now produced three separate ways. PAD_DB_DRIVER alone decides. Verified by
running the real command with the target exported.

**The preflight refused on tables the migration does not copy.**
ExportWorkspace / ImportWorkspace read six tables, and migrate-to-pg's own help
says users, platform settings and auth data are not migrated — so a NUL in
users.name blocked a copy that would never touch it, demanding the operator
rewrite content unrelated to the migration they asked for.

Refusal is now filtered to store.MigratedTables(). Those rows are still
REPORTED: `pad db scan-nul` lists them, they are real, and going quiet about a
broken row because this command does not care about it would be the
information-discarding the preflight was already corrected for once.

The table set is pinned by REFLECTION over models.WorkspaceExport's shape, not
by a regex over ExportWorkspace's SQL — TASK-2825 already established that
multi-line and Sprintf-composed SQL are invisible to any source-level
instrument. It fails in both directions: a new export section with no entry
(a miss, ending in a half-finished migration) and a spurious entry (an
over-refusal).

One residual, stated rather than hidden: the export also skips SOFT-DELETED
collections and items, and this filter is per-table. A NUL in a soft-deleted
item still blocks. Narrowing it needs a per-row deleted_at check at every
candidate, which costs more than the remaining over-refusal — the operator's
way out is the same single command either way.
2026-09-02 18:01:37 +00:00
..