@happyvertical/smrt-cli 0.47.1 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +16 -13
- package/agents/db-migrate-partial-apply.md +189 -0
- package/agents/db-orphans.md +46 -0
- package/agents/type-drift.md +53 -0
- package/dist/{commands-BiXRxejk.js → commands-DEHkYYWF.js} +921 -25
- package/dist/index.js +9 -9
- package/package.json +10 -9
package/AGENTS.md
CHANGED
|
@@ -10,16 +10,18 @@ smrt doctor # Umbrella diagnostics; can verify a generation sna
|
|
|
10
10
|
smrt doctor --db # Add the live-schema parity section (see below)
|
|
11
11
|
smrt db:status # Pending schema changes + failed migration classification
|
|
12
12
|
smrt db:status --parity # Same, plus live-schema parity (see below)
|
|
13
|
-
smrt db:
|
|
13
|
+
smrt db:orphans # agents/db-orphans.md
|
|
14
|
+
smrt db:migrate # agents/type-drift.md
|
|
14
15
|
smrt db:migrate --postgres-safe # PostgreSQL concurrent-index mode (see below)
|
|
15
16
|
smrt db:migrate --force-migration <exact-id> [--force-migration <exact-id>...] # Force exact generated migrations in one atomic batch
|
|
17
|
+
smrt db:migrate --apply-unblocked / --null-orphans # agents/db-migrate-partial-apply.md
|
|
16
18
|
smrt db:migrate-uuid # Convert schema-declared UUID text columns after data remap
|
|
17
19
|
smrt db:migrate-int8 # Explicitly widen pre-#2373 int4 columns after preflight
|
|
18
20
|
smrt db:drop-framework-base-tables # One-time drop of the five #2644-orphaned framework-base tables
|
|
19
21
|
smrt db:drop-framework-base-tables --dry-run # Print the drop plan without executing
|
|
20
22
|
smrt db:diff # Show schema differences without generating migration files
|
|
21
23
|
smrt db:rollback # Roll back migrations by executing their recorded DOWN
|
|
22
|
-
smrt db:rollback --mark-only # Record-only flip; schema
|
|
24
|
+
smrt db:rollback --mark-only # Record-only flip; schema untouched
|
|
23
25
|
smrt db:prune # Prune framework system tables to their retention windows
|
|
24
26
|
smrt db:prune --dry-run # Same predicates, counted rather than deleted
|
|
25
27
|
smrt docs:agents # Generate .agents/smrt-framework.md
|
|
@@ -50,7 +52,8 @@ cycles are created without the cyclic clauses first, then receive named
|
|
|
50
52
|
constraints after both tables exist. When a same-package constraint is missing
|
|
51
53
|
on an existing table, `db:migrate` probes the exact child/parent columns for
|
|
52
54
|
orphans before `ADD ... NOT VALID` and `VALIDATE CONSTRAINT`; orphaned data or a
|
|
53
|
-
failed probe stays manual with detector/repair SQL
|
|
55
|
+
failed probe stays manual with detector/repair SQL by default
|
|
56
|
+
(agents/db-migrate-partial-apply.md).
|
|
54
57
|
|
|
55
58
|
`db:migrate --dry-run` and deprecated `db:setup --dry-run` print the same
|
|
56
59
|
engine-specific dependency plan used for execution, including exact table DDL
|
|
@@ -326,33 +329,33 @@ drop `includeDroppedTables` (default `false`, both call sites explicit —
|
|
|
326
329
|
## Architecture
|
|
327
330
|
|
|
328
331
|
- **Lazy command loading**: commands loaded on-demand via dynamic import (~100ms overhead on first use)
|
|
329
|
-
- **Manifest discovery**:
|
|
330
|
-
- **Aggregate manifest identity**: preload
|
|
332
|
+
- **Manifest discovery**: finds `.smrt/manifest.json` + scans `node_modules/@happyvertical/smrt-*`
|
|
333
|
+
- **Aggregate manifest identity**: preload/schema discovery resolve package ownership per entry (`definition.packageName` before the container), keeping dependency objects' qualified registrations
|
|
331
334
|
- **Class loading order**: config.entryPoint → package.json exports['.'] → package.json main → `./dist/index.js`
|
|
332
|
-
- **Object method exposure**: custom
|
|
335
|
+
- **Object method exposure**: custom SMRT object methods auto-become CLI commands
|
|
333
336
|
|
|
334
337
|
## Key Files
|
|
335
338
|
|
|
336
|
-
- `src/cli-generator.ts` — core dispatcher, lazy command
|
|
339
|
+
- `src/cli-generator.ts` — core dispatcher, lazy command/class loading
|
|
337
340
|
- `src/commands/` — individual command implementations
|
|
338
341
|
- `src/loaders/` — class-loader, local-loader, npm-loader, git-loader, template-loader
|
|
339
342
|
- `src/discovery/manifest-discovery.ts` — manifest auto-discovery
|
|
340
|
-
- `src/commands/docs-claude.ts` — downstream AGENTS.md generation
|
|
343
|
+
- `src/commands/docs-claude.ts` — downstream AGENTS.md generation + Claude alias; no object scanning
|
|
341
344
|
|
|
342
345
|
## Gotchas
|
|
343
346
|
|
|
344
|
-
- **Test mode detection**: checks `NODE_ENV=test`, `VITEST=true`, `global.it`/`describe` —
|
|
345
|
-
- **External package load failures silenced**: one package failing doesn't
|
|
347
|
+
- **Test mode detection**: checks `NODE_ENV=test`, `VITEST=true`, `global.it`/`describe` — may conflict with other test runners
|
|
348
|
+
- **External package load failures silenced**: one package failing doesn't block others
|
|
346
349
|
- **Generation command names are hyphenated**: `generate-mcp` and `generate-types` have
|
|
347
|
-
no colon alias. Only `generate-routes
|
|
348
|
-
`smrt generate:mcp` is
|
|
350
|
+
no colon alias. Only `generate-routes`/`generate-register` declare one, so
|
|
351
|
+
`smrt generate:mcp` is unknown (#2279).
|
|
349
352
|
- **Subcommand `--version` beats the global flag**: `parseCliArgs` in
|
|
350
353
|
`@happyvertical/utils` dispatches to the built-in `version` command for a
|
|
351
354
|
`--version` token anywhere in argv. `parseCliCommandArgs` rescopes that token to
|
|
352
355
|
the named subcommand (`smrt generate-mcp --version 0.1.0`) and only leaves it
|
|
353
356
|
global when it appears before or without a subcommand (#2279). `--help` is
|
|
354
357
|
deliberately untouched.
|
|
355
|
-
- **Schema history nuance**: `db:status
|
|
358
|
+
- **Schema history nuance**: `db:status`/`db:history` should distinguish active live drift from superseded failed repairs, not treat every failed row as a current blocker
|
|
356
359
|
- **Decorator check follows the Vite major**: doctor requires `oxc.decorator` in
|
|
357
360
|
vite.config on Vite 8+ and accepts tsconfig `experimentalDecorators` only
|
|
358
361
|
below it. The old unconditional tsconfig check flagged correct Vite 8 projects
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# `db:migrate --apply-unblocked` and `--null-orphans` (#2748)
|
|
2
|
+
|
|
3
|
+
Both are opt-in and off by default; default `db:migrate` behavior is
|
|
4
|
+
unchanged either way. Implementation: `packages/cli/src/commands/
|
|
5
|
+
db-migrate-actions.ts` (pure partition/planning functions) and
|
|
6
|
+
`packages/cli/src/commands/utilities.ts`'s `db:migrate` handler (wiring,
|
|
7
|
+
I/O). Core side: `SchemaChange.orphanBlocked` / `.orphanNullable` /
|
|
8
|
+
`.engineUnsupported` (`packages/core/src/schema/types.ts`), set by the
|
|
9
|
+
FK advisory branches in `compareForeignKeys()`
|
|
10
|
+
(`packages/core/src/migrations/differ.ts`), and
|
|
11
|
+
`renderForeignKeyAddStatements()` (`packages/core/src/schema/
|
|
12
|
+
foreign-key-ddl.ts`).
|
|
13
|
+
|
|
14
|
+
## `--apply-unblocked`: partial apply
|
|
15
|
+
|
|
16
|
+
Today's unflagged batch already excludes every individually blocked change
|
|
17
|
+
(a FK-with-orphans, a manual `type_upgrade`/`alter_column`) — those never
|
|
18
|
+
leave `SchemaDiff.changes` as executable SQL, so they never enter the atomic
|
|
19
|
+
tracker batch on their own. `--apply-unblocked` adds a second, opt-in
|
|
20
|
+
partition on top of that: it withholds not just the blocked changes
|
|
21
|
+
themselves but anything that *depends* on one, and applies everything else.
|
|
22
|
+
|
|
23
|
+
**Dependency rule.** `computeBlockedColumns()` reads every manual
|
|
24
|
+
intervention's blocked `table.column` identity, plus every report-only
|
|
25
|
+
`type_upgrade` **advisory** (the #2608 refused uuid convergence — the only
|
|
26
|
+
producer of that shape) — an advisory-only finding never enters
|
|
27
|
+
`manualInterventions` (it carries no SQL) but names a live column just as
|
|
28
|
+
concretely, and a dependent index/alter/drop on that column is exactly as
|
|
29
|
+
unsafe as one on a manual-intervention column (review finding, #2748: the
|
|
30
|
+
advisory bucket was previously excluded, so `--apply-unblocked` did not
|
|
31
|
+
honor its own stated guarantee for that class of blocked type upgrade).
|
|
32
|
+
`partitionUnblockedMigrations()` then withholds any `add_index` /
|
|
33
|
+
`add_foreign_key` / `alter_column` / `drop_column` that reads or writes one
|
|
34
|
+
of those columns:
|
|
35
|
+
|
|
36
|
+
- an index on a column whose type upgrade is blocked (manual intervention
|
|
37
|
+
or report-only advisory);
|
|
38
|
+
- a foreign key whose child *or* parent column is blocked (a parent column
|
|
39
|
+
mid-type-upgrade is just as unsafe to reference as the child);
|
|
40
|
+
- an `alter_column`/`drop_column` on the blocked column itself.
|
|
41
|
+
|
|
42
|
+
**A report-only `alter_column` advisory is deliberately excluded from that
|
|
43
|
+
second loop**, even though `SchemaAdvisory` allows the type. Unlike
|
|
44
|
+
`type_upgrade`, the only producer of an advisory-only `alter_column` is an
|
|
45
|
+
un-opted-into relaxation (`drop_default`/`drop_not_null` when
|
|
46
|
+
`--relax-columns` was not passed) — it says the live column is *stricter*
|
|
47
|
+
than the manifest, not that anything about the column blocks convergence,
|
|
48
|
+
and it reappears on every run regardless of `--apply-unblocked`. Blocking on
|
|
49
|
+
it reproduced the exact `engineUnsupported` defect for a different shape:
|
|
50
|
+
permanently withholding unrelated executable DDL on that column, worse than
|
|
51
|
+
omitting the flag (review finding, #2748, second pass). A genuinely blocked
|
|
52
|
+
`alter_column` — NOT NULL required with live NULLs and no default to
|
|
53
|
+
backfill — carries its refusal as comment-only SQL and already reaches
|
|
54
|
+
`manualInterventions` (keyed by `columnName`), so it was never affected.
|
|
55
|
+
|
|
56
|
+
`add_column` and an executable `type_upgrade` never depend on another
|
|
57
|
+
column's state — a new column add is self-contained, and a type upgrade is
|
|
58
|
+
the fix itself — so they always apply. Everything independent goes through
|
|
59
|
+
the exact same transaction/tracker path an unflagged run uses.
|
|
60
|
+
|
|
61
|
+
**An `add_foreign_key` blocked only because this engine cannot express the
|
|
62
|
+
constraint at all is not a column-state block.** SQLite (table rebuild
|
|
63
|
+
required) and DuckDB (no `ALTER TABLE ADD CONSTRAINT`) each carry
|
|
64
|
+
`engineUnsupported: true` on that advisory — distinct from a conflicting
|
|
65
|
+
constraint or an incompatible-type block, which genuinely do implicate the
|
|
66
|
+
column and stay blocking. `blockedColumnForAction()` excludes only the
|
|
67
|
+
`engineUnsupported` case: every real differ `add_foreign_key` advisory sets
|
|
68
|
+
`foreignKey`, so gating on its mere presence blocked this case too (review
|
|
69
|
+
finding, #2748) and, since no rerun on that engine ever resolves it, made
|
|
70
|
+
`--apply-unblocked` permanently withhold unrelated dependent DDL on that
|
|
71
|
+
column — strictly worse than omitting the flag.
|
|
72
|
+
|
|
73
|
+
**An orphan-blocked `add_foreign_key`'s block is a row-data condition, not
|
|
74
|
+
a column-state one — an `alter_column` relaxation on that same column is
|
|
75
|
+
its remediation, not something unsafe against it.** With `--relax-columns
|
|
76
|
+
--apply-unblocked`, a FK child column that is physically `NOT NULL` while
|
|
77
|
+
the manifest declares it nullable (with live orphan rows) stays
|
|
78
|
+
orphan-blocked — `--null-orphans` can't resolve a NOT NULL child. The
|
|
79
|
+
executable `DROP NOT NULL` relaxation for that same column is exactly what
|
|
80
|
+
makes the block eventually resolvable (the column becomes nullable, then a
|
|
81
|
+
later `--null-orphans` run can null the references). Gating that
|
|
82
|
+
relaxation on the orphan block (review finding, #2748) made the run
|
|
83
|
+
withhold it on every pass, so the column never became nullable, the
|
|
84
|
+
orphan block never cleared, and the batch failed forever — worse than
|
|
85
|
+
omitting the flag. `computeOrphanOnlyBlockedColumns()` identifies a
|
|
86
|
+
`table.column` whose *only* manual-intervention block is an orphan-blocked
|
|
87
|
+
FK on that column, and `partitionUnblockedMigrations()` excludes that key
|
|
88
|
+
from gating an `alter_column` specifically — an index, a different foreign
|
|
89
|
+
key, or a `drop_column` on that same orphan-blocked column still correctly
|
|
90
|
+
stays withheld.
|
|
91
|
+
|
|
92
|
+
Withheld items print under `🔒 Withheld — depends on a blocked change`, each
|
|
93
|
+
naming the `table.column` it depends on and the same reason text the manual
|
|
94
|
+
intervention above it carries. `--dry-run` shows the identical
|
|
95
|
+
applied/withheld partition (the partition is pure and runs before the
|
|
96
|
+
dry-run/apply branch splits). A rerun is a no-op for what already applied
|
|
97
|
+
and repeats the same withheld list until the underlying blocker is resolved
|
|
98
|
+
— nothing here is remembered between runs; the partition is recomputed from
|
|
99
|
+
the live diff every time.
|
|
100
|
+
|
|
101
|
+
## `--null-orphans`: opt-in orphan-FK disposition
|
|
102
|
+
|
|
103
|
+
Targets one specific blocked-item shape: a foreign key `db:migrate` would
|
|
104
|
+
otherwise refuse only because live child rows don't match any parent row
|
|
105
|
+
(`SchemaChange.orphanBlocked`). `planOrphanDispositions()` splits manual
|
|
106
|
+
interventions into nullable (a disposition is possible) and not-nullable
|
|
107
|
+
(unconditional refusal) using `SchemaChange.orphanNullable` — never by
|
|
108
|
+
parsing `advisory.message` text.
|
|
109
|
+
|
|
110
|
+
For a nullable child column: the *exact* `UPDATE ... SET <column> = NULL
|
|
111
|
+
WHERE ...` the differ already generated for that relationship
|
|
112
|
+
(`advisory.suggestedSql[1]` — never re-derived, so the disposition can never
|
|
113
|
+
drift from what the differ decided the safe repair is) and the `ADD
|
|
114
|
+
CONSTRAINT ... NOT VALID` + `VALIDATE CONSTRAINT` pair from
|
|
115
|
+
`renderForeignKeyAddStatements()` (the same helper the differ's own safe-add
|
|
116
|
+
branch uses) are combined into **one** `add_foreign_key` migration action —
|
|
117
|
+
`sqlStatements: [repairSql, ...addStatements]` — and applied atomically
|
|
118
|
+
through the normal tracker path, not run as a separate direct query before
|
|
119
|
+
it (review finding, #2748: running the UPDATE outside the batch let a later
|
|
120
|
+
failure elsewhere in the same batch roll back the FK add while leaving the
|
|
121
|
+
just-nulled rows committed, silently splitting one promised disposition
|
|
122
|
+
into two). The resolved FK moves from `manualInterventions` into the
|
|
123
|
+
executable `migrations` bucket before the `--apply-unblocked` partition
|
|
124
|
+
runs, so a relationship this flag resolves is never a blocked column for
|
|
125
|
+
that dependency rule. Post-apply before/after counts are only printed once
|
|
126
|
+
the batch that carries this disposition has actually committed; on a full
|
|
127
|
+
rollback the generic atomic-failure message already covers it — nothing in
|
|
128
|
+
that batch, including this disposition, applied.
|
|
129
|
+
|
|
130
|
+
**The combined migration can still be withheld by `--apply-unblocked`
|
|
131
|
+
itself**, if the FK's *parent* column is separately blocked (e.g. a
|
|
132
|
+
report-only `type_upgrade` advisory) — resolving out of
|
|
133
|
+
`manualInterventions` only means this specific relationship's *child*-side
|
|
134
|
+
orphan block is gone, not that the dependency partition can't withhold it
|
|
135
|
+
for an unrelated reason. `filterUnresolvedOrphanDispositions()` drops a
|
|
136
|
+
pending post-apply report entry whose exact migration action ended up
|
|
137
|
+
withheld, matched by object identity (the same reference is pushed into
|
|
138
|
+
both `migrations` and the pending entry) rather than by table/column, so
|
|
139
|
+
the report never prints a `✓ ... resolved` line for a migration that never
|
|
140
|
+
executed (review finding, #2748) — that relationship is already covered by
|
|
141
|
+
the `🔒 Withheld` listing.
|
|
142
|
+
|
|
143
|
+
**"Committed" is checked per disposition, not for the whole batch.** In
|
|
144
|
+
PostgreSQL concurrent-index mode (`--postgres-safe` with
|
|
145
|
+
`migrations.postgres.useConcurrently`), the non-index batch — including
|
|
146
|
+
this disposition's combined migration — commits in its own transaction
|
|
147
|
+
before any deferred `CREATE INDEX CONCURRENTLY` migrations run separately
|
|
148
|
+
and non-transactionally; a later index build failure still increments
|
|
149
|
+
`errorCount` even though the non-index changes already committed. Gating
|
|
150
|
+
the report on `errorCount === 0` alone silently suppressed the resolution
|
|
151
|
+
line for a mutation that did commit (review finding, #2748). A batch-wide
|
|
152
|
+
proxy for "did the non-index phase commit" is not enough either: it can't
|
|
153
|
+
tell that apart from the non-index transaction itself rolling back (recall
|
|
154
|
+
finding, #2748) — both raise `errorCount`, but only one means this
|
|
155
|
+
disposition's own migration applied. The report instead re-derives each
|
|
156
|
+
pending disposition's synthetic migration name
|
|
157
|
+
(`getSyntheticMigrationNameForAction()`, the same helper that named it when
|
|
158
|
+
building the tracker batch) and checks that exact name's own `success` in
|
|
159
|
+
`tracker.applyAll()`'s per-migration results — the ground truth regardless
|
|
160
|
+
of what else in the batch failed — printing a caveat line only when some
|
|
161
|
+
other change in the batch failed but this disposition's own migration
|
|
162
|
+
still succeeded.
|
|
163
|
+
|
|
164
|
+
For a `NOT NULL` child column: refuse with the same "Manual repair required"
|
|
165
|
+
text `db:migrate` always prints for that case. Nulling is not a legal repair
|
|
166
|
+
there, and **this flag never deletes rows** either way — the only mutating
|
|
167
|
+
statement is the UPDATE that nulls references, never a DELETE.
|
|
168
|
+
|
|
169
|
+
`--dry-run` prints the exact UPDATE statement plus the live orphan count
|
|
170
|
+
(`orphanCountSql()` wraps the differ's read-only detector SELECT as a
|
|
171
|
+
`COUNT(*)`) before applying anything; a real run reports the count before
|
|
172
|
+
and after. Composes with `smrt db:orphans` (#2753, `packages/cli/agents/
|
|
173
|
+
db-orphans.md`) — same probe, same repair SQL, no duplicated logic.
|
|
174
|
+
|
|
175
|
+
**Fails closed on the count probe too** (review finding, #2748):
|
|
176
|
+
`countOrphanRows()` throws rather than swallowing a query failure into `0`.
|
|
177
|
+
An orphan-count probe that cannot run at all is exactly the "unexpected
|
|
178
|
+
shape" case this flag must report and withhold on — proceeding to null data
|
|
179
|
+
on an unverified count would violate the fail-closed requirement. A probe
|
|
180
|
+
failure withholds that one relationship's disposition (printed, left in
|
|
181
|
+
`manualInterventions`) without aborting the rest of the batch or the other
|
|
182
|
+
dispositions.
|
|
183
|
+
|
|
184
|
+
## Fails closed on an unexpected shape
|
|
185
|
+
|
|
186
|
+
`planOrphanDispositions()` routes an orphan-blocked, nullable change to the
|
|
187
|
+
refusal bucket (never guesses SQL) if it is missing its `foreignKey`
|
|
188
|
+
definition or its `advisory.suggestedSql` pair — a differ-side contract
|
|
189
|
+
violation, not something this disposition should paper over.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# `db:orphans` — per-foreign-key orphan count report (#2753)
|
|
2
|
+
|
|
3
|
+
`db:migrate`'s orphan probe already gates `ADD CONSTRAINT` with a `LIMIT 1`
|
|
4
|
+
existence check and prints a suggested repair (see the PostgreSQL section of
|
|
5
|
+
`packages/cli/AGENTS.md`); it cannot say *how many* orphans exist or across
|
|
6
|
+
which relationships. `db:orphans` runs core's `collectForeignKeyOrphanCounts()`,
|
|
7
|
+
which probes every manifest-declared foreign key from
|
|
8
|
+
`ObjectRegistry.getAllSchemasAsDefinitions()` as a `COUNT(*)` aggregate —
|
|
9
|
+
reusing `renderForeignKeyOrphanDetector()`'s exact FROM/JOIN/WHERE predicate
|
|
10
|
+
via its `countOnly` option, never a second hand-written copy — and reports
|
|
11
|
+
child/parent table and column, the live orphan count, and whether the child
|
|
12
|
+
column is `NOT NULL` (so the two dispositions the repair-SQL builder already
|
|
13
|
+
distinguishes — manual repair vs. an executable null-out — are visible up
|
|
14
|
+
front, without hand-writing the same SQL against the database). Results print
|
|
15
|
+
sorted by count descending. A relationship whose child or parent table does
|
|
16
|
+
not exist live is skipped and listed separately rather than failing the whole
|
|
17
|
+
report.
|
|
18
|
+
|
|
19
|
+
Strictly read-only and diagnostic: it never repairs anything, and the orphan
|
|
20
|
+
counts themselves never affect the exit code — a database full of orphans
|
|
21
|
+
still exits 0, matching the existing "no foreign-key orphan repair, by
|
|
22
|
+
design" boundary. It exits non-zero only on a configuration or runtime error
|
|
23
|
+
(no database configured, or the report itself could not run). `--json` emits
|
|
24
|
+
the full report for scripting; `--verbose` also lists foreign keys with zero
|
|
25
|
+
orphans.
|
|
26
|
+
|
|
27
|
+
`ForeignKeyOrphanSkipped.kind` distinguishes a benign `missing_table` skip
|
|
28
|
+
(a manifest table not yet migrated) from `probe_failed` (the `COUNT(*)`
|
|
29
|
+
query itself errored — permissions, a malformed live column, a genuine SQL
|
|
30
|
+
failure). `smrt db:orphans` marks a `probe_failed` skip `[PROBE FAILED]` in
|
|
31
|
+
its Skipped section and never reports the "No orphan rows found" clean
|
|
32
|
+
summary while any probe has failed, since an incomplete scan is not the same
|
|
33
|
+
claim as a clean one.
|
|
34
|
+
|
|
35
|
+
`db:status` runs the same probe unconditionally (not gated behind `--parity`,
|
|
36
|
+
and independent of whether the configured adapter supports
|
|
37
|
+
`getTableSchema` — the probe only needs `db.query()`) and, when any manifest
|
|
38
|
+
foreign key has live orphans, prints one compact line per affected foreign
|
|
39
|
+
key plus a pointer to `smrt db:orphans` for the full report. A `probe_failed`
|
|
40
|
+
skip surfaces as its own distinct warning (`status.orphanProbeFailures`)
|
|
41
|
+
rather than being read as "no orphans found"; `missing_table` skips stay
|
|
42
|
+
silent in `db:status` (benign, and already visible in full via
|
|
43
|
+
`smrt db:orphans`). Never a new `SchemaDiff` finding kind, and never
|
|
44
|
+
touching `db:status`'s exit code. A total probe failure (the collector call
|
|
45
|
+
itself throws, e.g. it cannot list live tables at all) is reported as
|
|
46
|
+
"Foreign-key orphan check unavailable" rather than failing `db:status`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Float-width, timestamptz, and jsonb convergence (#2770, #2771, #2772)
|
|
2
|
+
|
|
3
|
+
## Float-width drift (#2770)
|
|
4
|
+
|
|
5
|
+
`db:status`/`db:diff` report float-width drift (`real` vs `double precision`)
|
|
6
|
+
as `column_type_drift`, the same severity class as `legacy_integer_width`; a
|
|
7
|
+
live single-precision column backed by a double-precision declaration widens
|
|
8
|
+
automatically (lossless), but a double-precision live column backed by a
|
|
9
|
+
single-precision declaration stays advisory-only (narrowing loses precision).
|
|
10
|
+
|
|
11
|
+
## text -> timestamptz convergence (#2771)
|
|
12
|
+
|
|
13
|
+
A `text` column on a table smrt itself creates, backed by a manifest
|
|
14
|
+
`TIMESTAMP` (-> `TIMESTAMPTZ` on PostgreSQL) declaration, converges instead of
|
|
15
|
+
staying blocked forever: `db:migrate` runs a server-side shape probe over
|
|
16
|
+
every non-null value first (`packages/core/src/schema/text-cast-probe.ts`)
|
|
17
|
+
and only plans the executable `ALTER COLUMN … TYPE timestamptz USING
|
|
18
|
+
col::timestamptz` when every value is confirmed to parse unambiguously. A
|
|
19
|
+
probe that finds an unparsable value fails closed with a masked sample
|
|
20
|
+
instead of running the cast — repair the value, then rerun; the migration is
|
|
21
|
+
a no-op once converged. This is independent of
|
|
22
|
+
`postgresTimestampMigration.legacyTimezone`
|
|
23
|
+
(`--postgres-timestamp-legacy-timezone=UTC`), which
|
|
24
|
+
stays reserved for ambiguous naive wall-clock strings the probe does not
|
|
25
|
+
accept. SQLite has no distinct storage for `text` vs `timestamptz`, so this
|
|
26
|
+
stays advisory-only there.
|
|
27
|
+
|
|
28
|
+
## text -> jsonb convergence and uuid visibility (#2772)
|
|
29
|
+
|
|
30
|
+
A `text` column on a table smrt itself creates, backed by a manifest `JSON`
|
|
31
|
+
(-> `jsonb` on PostgreSQL) declaration, was previously invisible to
|
|
32
|
+
`db:status`/`db:diff` and had no `db:migrate` repair path (the #1335
|
|
33
|
+
text/json tolerance is directional: it stays silent for the reverse pairing —
|
|
34
|
+
a native `json`/`jsonb` live column backed by a text-convention manifest
|
|
35
|
+
field — but this direction now runs the same shared shape probe as #2771's
|
|
36
|
+
timestamptz path and converges the same way: a clean probe plans `ALTER
|
|
37
|
+
COLUMN … TYPE jsonb USING col::jsonb`; a dirty probe fails closed with a
|
|
38
|
+
masked sample. `db:status` also now reports an `info`-level finding for a
|
|
39
|
+
declared-`uuid` structural column still backed by live `text` (previously
|
|
40
|
+
silent by design), pointing at `db:migrate-uuid` — the tolerance itself is
|
|
41
|
+
unchanged, only its visibility. SQLite stays advisory-only for both.
|
|
42
|
+
|
|
43
|
+
## Live-only column defaults on either conversion
|
|
44
|
+
|
|
45
|
+
PostgreSQL rejects `ALTER COLUMN … TYPE` outright whenever the column has
|
|
46
|
+
any existing default that can't auto-cast to the target type, so both
|
|
47
|
+
conversions above always `DROP DEFAULT` first when the *live* column has
|
|
48
|
+
one — restoring it afterward (`SET DEFAULT`) only when the *manifest* also
|
|
49
|
+
declares one. A live-only default (the manifest declares none) is the same
|
|
50
|
+
"relaxation" `db:status --parity`'s default-drift finding already gates
|
|
51
|
+
behind `relaxColumns`/`--relax-columns` elsewhere: without that opt-in, the
|
|
52
|
+
conversion stays a fail-closed advisory naming the blocking default instead
|
|
53
|
+
of executing.
|