Commit fe71032
Fixes #15989
Clause-②: yes
Step 2 of the maintainer ruling on #15041, completed. PR #17403 landed
the encoding half; this lands the two things it named as still owed —
**the kernel→driver supply seam** and **the column step** — under the
director ruling `5643444495` (decision batch #120 item 1).
## ⛔ The ruling supersedes the card body in two places, and both are
honoured
**1 · Keying.** The card body's "Shape of the change" item 1 says the
wiring is keyed on the `adr-0104-file-references` flag. That is
**overturned**. The arm is keyed on `sys_migration.columns_moved_at`,
and it additionally requires the flag to be verified — never the flag
alone. The reason is measured, not formal: every creation-attested store
since 17.0 holds that flag **and** JSON-quoted ids in a JSON column, so
flag-keying would read every existing deployment as migrated and then
write bare ids into a JSON column.
`adr0104-file-columns-moved-supply.test.ts` pins exactly that row —
verified, unstamped — as "not moved".
**2 · The migration SQL.** The addendum's `USING (col #>> '{}')` is
**not used bare**. It is preceded by the pre-check the dev seat
measured, which aborts on a non-JSON-string cell. See the ablation
below: the superseded form was executed on live PostgreSQL 16.13 in this
PR's own suite, and it does not abort.
⭐ Worth carrying forward, in the card's own words: *"an Execution
paragraph is authoritative about intent, not immune to being wrong about
SQL."*
## The ablation — the single most important measurement here
Both clauses, executed on one fixture (one converted cell, one inline
blob the backfill has not reached), in
`sql-driver-15989-file-column-move.test.ts` §1, on every dialect this
step serves.
| clause | what happened |
|:--|:--|
| **superseded** — `ALTER … TYPE varchar(2048) USING (col #>> '{}')`,
alone | **ACCEPTED**, exit 0, no warning. The column is no longer
`json`, so the unconverted object is now a plain string in a column
whose declared contents are bare ids. |
| **this PR** — the same DDL behind `json_typeof(col) IS DISTINCT FROM
'string'` | pre-check answers **1** ⇒ the step runs **no statement**.
Cells byte-identical, column still `json`. |
| **CONTROL** — the same pre-check on a fixture with no unconverted row
| answers **0** ⇒ the move proceeds, producing bare ids, `NULL`
preserved. So the `1` above is a reading, not a pre-check that always
blocks. |
⚠️ **What is lost differs by dialect, and the PR states the two
separately rather than averaging them.** On PostgreSQL the `json` type
stores its input text verbatim and `#>> '{}'` hands that same text back,
so the **bytes survive and the TYPE is destroyed** — which is exactly
why the defect is invisible to a byte diff. On SQLite there is no type
to lose, so the loss is in the **bytes**: `json_extract` re-serialises
the blob and its formatting is gone. Either way the superseded form
wrote to a row it was required to refuse.
Reproduced outside the suite first, against a cluster started for the
purpose (PostgreSQL 16.13, port 54331, torn down afterwards); the
readings are in the report.
## The seam — "every way of not knowing answers not moved"
⚠️ **Why the wiring is owed: ruling item 2, not a zero-hit reading.**
`fileColumnsMoved` had no supplier outside `driver-sql` on the dispatch
base — re-measured here with a same-subject control, asking the
identical question of each of `SqlDriverConfig`'s own added keys over
the same corpus and package boundary: `schemaMode` 72 files ·
`autoMigrate` 14 · `sqliteAbsentFile` 6 (the firing control, and it
includes a real host wiring site,
`service-datasource/src/default-datasource-driver-factory.ts`) ·
`sqliteJournalMode` **0** · `fileColumnsMoved` **0**. ⛔ So the zero
proves only that there was no supplier — `sqliteJournalMode` reads zero
too and is a perfectly ordinary optional key no host sets. What makes
this one owed is the director ruling's item 2, which places the
kernel→driver wiring inside this dispatch rather than after it.
`ObjectQL.registerDriver` hands every driver that has the seam a closure
over the new `ObjectQL.haveFileColumnsMoved()`. Pinned on both sides —
thirteen engine-side cases and twelve driver-side ones:
- option omitted · resolver throws · resolver rejects · resolver answers
a non-`true` value · resolver never runs (the host never calls
`initObjects`) · driver has no seam at all;
- no `sys_migration` object · no row · unreadable table · null stamp ·
empty-string stamp · stamp on an unverified row · stamp on a row with
blocking findings;
- ⭐ and the **control** in both files: a verified row **with** a stamp
answers `true`, so every `false` above is a reading rather than a
welded-shut arm.
⛔ **A host that names `fileColumnsMoved` in its own config wins, in
either polarity.** The engine only fills an empty slot. Overruling a
declared `false` is the bare-ids-into-a-JSON-column failure this whole
mechanism exists to prevent; overruling a declared `true` writes JSON
into columns already retyped. The host is the more specific authority
about its own storage, so the engine never contradicts it.
A resolver arriving **after** the first `initObjects` is also refused:
`registerObjectMetadata` has already frozen `isJsonField`'s answer for
every media column, so a late resolver would be a promise the driver
cannot keep.
## The column step
`os migrate files-to-references --apply` gains it, and it moves nothing
until three gates pass: the migration's own gate (zero blocking rows),
**every** pre-check across **every** planned column, and no refusals.
Two-phase on purpose — a step that moved three columns and aborted on
the fourth leaves a datastore in a state no flag can describe.
The shape is read off the column's **physical type**, never off the
dialect: a `json` column is retyped; a column already `varchar` — the
population `os generate migration --format sql` creates and a JSON-arm
driver fills with quoted ids (#15771) — has its values unquoted in
place. Classifying by dialect would have left that population full of
quoted ids behind a `columns_moved_at` stamp claiming it was converted.
⛔ **MySQL is out of scope and refused by name**, per ruling item 3 —
#17788 owns it, on a real instance, because the addendum leaves its
statement ORDER unsettled. `mediaColumnMoveDialect('mysql')` answers
`null` and the step reports a named refusal. §5 pins that refusal so
nobody "completes the matrix" by transcribing a form nobody has run —
which is the move that produced the Postgres clause this card had to
overturn.
## Per-dialect pin results
`sql-driver-15989-file-column-move.test.ts`, both arms, both encodings,
across the window:
- **SQLite** — 7 passed, 1 skipped (the unprovisioned PG cell, named).
- **live PostgreSQL 16.13** — 13 passed, 0 skipped, with
`OS_TEST_POSTGRES_URL` provisioned.
- ⛔ **MySQL — NOT MEASURED, and not a cell.** No `mysqld`/`mariadb`
binary and no docker daemon in this container; more to the point the
step has no MySQL statements to measure. This is a named absence, never
a pass and never a fail.
§3 is the reverse verification the card asks for: after the move, a
driver on the **moved** arm writes a bare id and reads it back
unchanged, and reads the row the move itself converted; §3b holds the
other half of the window, where an **unmoved** deployment still stores
and reads its JSON-quoted id.
⚠️ One fixture correction worth naming: the SQLite cell is `:memory:`,
so two driver instances are two **separate** databases — a §3 written
against it would have been green while measuring nothing (the second
driver creates its own table, its own writes read back perfectly, and
the migrated rows are simply absent). The suite uses a file-backed
SQLite database so the two arms share bytes, as the live cells already
do.
## Changeset
`.changeset/15989-file-family-column-step.md` — `minor` on the five
packages that publish a change, with the launch-window `**BREAKING**`
banner, and the ADR-0087 disposition **not-required
(no-migration-prescription)** derived from this diff: nothing authorable
moves. `DataMigrationFlagSchema` and its `columns_moved_at` member
landed under #16185 and are read here, not edited; the one
`packages/spec` edit is a new exported predicate function over that
existing type. So `os migrate meta` has nothing to visit,
`spec-changes.json` nothing to project, the upgrade guide no row to
gain. The other four categories are closed on facts in the changeset
itself.
## Verification
Every exit code captured by redirect-then-`$?`, never across a pipe.
**Suites — each package's own `test` target, unnarrowed. ⛔ No
`--project` filter anywhere** (a `--project` filter silently drops a
named file outside that project and reports it as passing — #17853).
| package | tiers | files | tests |
|:--|:--|--:|--:|
| `@objectstack/driver-sql` | single | 182 passed, 3 skipped (185) |
3213 passed, 81 skipped (3294) |
| `@objectstack/cli` | **unit + integration, both** | 246 passed (246) |
3247 passed (3247) |
| `@objectstack/objectql` | single | 298 passed (298) | 4990 passed
(4990) |
| `@objectstack/spec` | single | 472 passed (472) | 13434 passed (13434)
|
| `@objectstack/platform-objects` | single | 40 passed (40) | 571 passed
(571) |
`driver-sql` ran against **live PostgreSQL 16.13** with the server at
`Asia/Shanghai` and the process at `America/New_York`, matching how
`ci.yml` provisions the live matrix — the live suites assert a three-way
zone skew and are vacuous on a UTC server, which is a guard, not an
obstacle.
⛔ **MySQL: NOT MEASURED.** No `mysqld`/`mariadb` binary and no docker
daemon in this container — and the column step has no MySQL statements
to measure in the first place (#17788). A named absence: never a pass,
never a fail.
**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **103** families for this diff; all
103 were run and reconciled with `--ran`, each carrying its real exit
code, so the NOT-MEASURED zero is derived rather than claimed. ⛔ Four
hit `exit 3` (PREREQUISITE NOT MET — never a pass) and went green after
a full `pnpm build`: `check:dual-build-cjs-loads`, `check:i18n`,
`check:i18n-coverage`, `check:i18n-walk-parity`.
**Four gates went genuinely RED on my own first draft, and are recorded
rather than smoothed over:**
- `check:doc-authoring` — a tracker id in two runtime strings an
operator reads. Fixed by removing the id from the driver's refusal text
and by writing the engine's log literal once instead of duplicating it.
- `check:engine-double-contract` / `check:objectql-double-limit` /
`check:where-matcher` — all three on my new fakes. They now route
`update()` through `assertEngineUpdateDispatch`, apply the caller's
`limit` by presence after the filter, and REFUSE a combinator they do
not implement instead of reading it as a field name. The last one
matters here more than usual: a matcher that answers "no rows" for a
filter it does not understand is indistinguishable from an empty ledger,
which is exactly the false "not moved" every case in those files is
trying to tell apart from a real reading.
**Cross-package reverse verification** (an additive optional member on a
type crossing a package boundary is exactly the shape a cached `.d.ts`
answers the same way twice, so both legs were run): planting
`MediaColumnMoveScan['plansZZ']` in `@objectstack/cli` goes RED with
`TS2339: Property 'plansZZ' does not exist on type
'MediaColumnMoveScan'` (leg 2 exit 1), while the real member typechecks
(leg 1 exit 0) — so `tsc` is reading `driver-sql`'s rebuilt declaration,
not a cached one. Restored under a `trap … EXIT INT TERM` with
repo-absolute paths via `git checkout HEAD -- path`, and the restore
proven by state rather than by an exit code: `git hash-object` back to
`88f1ad9944` byte-identical to the HEAD blob, `git diff HEAD` empty.
All gate figures above are read at the final commit `303d7946e4`; the
reconciliation and the suites were re-run on the head that produced them
rather than on the tree that first did. The last commit is a one-line
correction inside the ADR-0087 marker — the seat's review measured my
rationale's counts as understated, I re-measured them independently off
the three-dot diff and got the same answer, and the marker now NAMES
what it counts so a reader can recount it: **13** new declarations
reaching a package entry (11 on `@objectstack/driver-sql`'s entry — 6
values and 5 types — plus `recordFileColumnMove` on
`@objectstack/platform-objects/system` and `hasMovedFileColumns` on
`@objectstack/spec`) and **3** new public methods on exported classes
(`SqlDriver.planMediaColumnMove`,
`SqlDriver.setFileColumnsMovedResolver`, `ObjectQL.haveFileColumnsMoved`
— none private or protected, on classes exported at `sql-driver.ts:4514`
and `engine.ts:2768`). The level, the BREAKING banner and the
disposition itself are unchanged; the clause those counts support is
simply more true at 13/3 than at the 5/2 I first wrote.
Control-byte self-scan over all changed files: zero hits, with a control
that fires on an in-class byte (0x0B). `pnpm check:nul-bytes` exit 0.
## Acceptance notes
- **filed as #17869:** a non-`Command` module under
`packages/cli/src/commands/` makes EVERY `os` invocation warn
`findCommand … not found` on stderr, because the oclif command table is
a `**/*.js` glob — and it corrupts machine-readable output for any
consumer that reads stdout and stderr together (`os validate --json`
then fails `JSON.parse`). Found because my own first draft put the
column-step helper there; that instance is fixed in this PR by moving it
to `src/utils/`, where every other CLI helper already lives. The missing
GUARD is the card: `check:cli-command-ids` walks the same population but
asks the opposite question, and the trap was caught by exactly one of
3247 CLI cases. Dedup ran before filing, with a firing control.
- `noted, not filed:` the docblock on `SqlDriverConfig.fileColumnsMoved`
pointed at `SqlDriver.setFileColumnsMoved`, a method that did not exist
— a dangling link left when the option landed without its supply.
Repaired in place by this PR, since the method the link wanted is the
one this PR adds. Successor: none needed, it is fixed here.
- `noted, not filed:` `readDataMigrationFlag` dropped `columns_moved_at`
on the way out, so a moved deployment was indistinguishable from an
unmoved one to every caller reading the ledger through it. Fixed here
because the column step is the first caller that has to tell them apart.
Successor: none needed.
- `noted, not filed:` the TS and SQL halves of the generator still
disagree on this family's width — `generateMigrationSql` emits
`VARCHAR(2048)` while `generateMigrationTs` emits knex's `varchar(255)`.
Carried from the #17403 round, which named the column-step seat as its
successor. This PR retypes to 2048, the SQL half's width and the one the
ruling calls the end-state, so the disagreement is now between the
generator's two halves alone. Successor: a
`packages/cli/src/commands/generate.ts` card; the file is untouched
here.
- `noted, not filed:` `os migrate plan` prose for a media column still
describes the JSON end-state, generated from the drift entry's message.
Correct for an unmoved deployment and wrong once one can be moved —
which this PR makes possible for the first time. Successor: the
drift-message half of #16184, which rewrites that arm.
Authored by the `domain:engine` execution seat, session
`https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM`. (Carried in
prose: on this repository the PR-body UPDATE channel does not recognise
the session-URL footer block and appends a second, bare one — measured
on the create/update pair for this very PR, so the platform's own
appended footer below is left as the single one.)
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e7fea46 commit fe71032
20 files changed
Lines changed: 2697 additions & 15 deletions
File tree
- .changeset
- packages
- cli/src
- commands/migrate
- utils
- drivers/driver-sql/src
- objectql/src
- platform-objects/src/system
- spec
- api-surface
- export-origins
- src/system
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
Lines changed: 230 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
22 | 58 | | |
23 | 59 | | |
24 | 60 | | |
| |||
221 | 257 | | |
222 | 258 | | |
223 | 259 | | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
224 | 280 | | |
225 | 281 | | |
226 | 282 | | |
| |||
250 | 306 | | |
251 | 307 | | |
252 | 308 | | |
| 309 | + | |
| 310 | + | |
253 | 311 | | |
254 | 312 | | |
255 | | - | |
| 313 | + | |
256 | 314 | | |
257 | 315 | | |
258 | 316 | | |
| |||
291 | 349 | | |
292 | 350 | | |
293 | 351 | | |
| 352 | + | |
| 353 | + | |
294 | 354 | | |
295 | 355 | | |
296 | | - | |
| 356 | + | |
297 | 357 | | |
298 | 358 | | |
299 | 359 | | |
| |||
303 | 363 | | |
304 | 364 | | |
305 | 365 | | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
306 | 534 | | |
0 commit comments