|
| 1 | +--- |
| 2 | +"@objectstack/spec": patch |
| 3 | +"@objectstack/objectql": patch |
| 4 | +"@objectstack/driver-sql": patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(driver-sql,spec,objectql): a `defaultValue` runtime token never becomes a column DEFAULT (#4560) |
| 8 | + |
| 9 | +`Field.user({ defaultValue: 'current_user' })` is resolved by the **engine**, at |
| 10 | +insert time, from the request's `ExecutionContext` — and with no authenticated |
| 11 | +user (system / anonymous writes: seed replay, package install, boot |
| 12 | +provisioning) `applyFieldDefaults` deliberately leaves the field **unset** |
| 13 | +rather than stamp a bogus owner. |
| 14 | + |
| 15 | +The SQL DDL had never heard of the token. `createColumn` passed any non-object |
| 16 | +`defaultValue` straight through to `col.defaultTo(dv)`, so the column was |
| 17 | +created as `DEFAULT 'current_user'` and the **database** overrode the engine's |
| 18 | +decision: every insert that omitted the field stored the literal string |
| 19 | +`current_user` in a `lookup('sys_user')` column — a value that is not any user's |
| 20 | +id. `?expand` resolves it to nothing, and on an owner / approver field it is a |
| 21 | +silent mis-attribution. Found by #4551's dangling-reference audit on its first |
| 22 | +run against a real boot; #4441's referential check could never have caught it, |
| 23 | +because it inspects the values a **caller** supplied and here nobody supplied |
| 24 | +one. |
| 25 | + |
| 26 | +**The token vocabulary is now declared once, in `@objectstack/spec/data`** |
| 27 | +(`DEFAULT_VALUE_TOKENS`, `isRuntimeDefaultToken`, `isNowDefaultToken`, |
| 28 | +`isCurrentUserDefaultToken`, `isAppResolvedDefaultToken`). The engine's |
| 29 | +insert-time resolution and the driver's DDL read the same set, which is the |
| 30 | +actual defect: `'NOW()'` was special-cased in the branch immediately above for |
| 31 | +precisely this reason, and `current_user` — the same convention family — simply |
| 32 | +had no entry anywhere the DDL could see. A token added to the set tomorrow is |
| 33 | +excluded from literal column DEFAULTs automatically, rather than leaking its own |
| 34 | +spelling into the database the way this one did. |
| 35 | + |
| 36 | +**DDL, in one place** (`applyDeclaredColumnDefault`, shared by column creation |
| 37 | +and the SQLite table rebuild): |
| 38 | + |
| 39 | +- `'NOW()'` → the driver-native canonical default, exactly as before; |
| 40 | +- any other runtime token → **no column default at all** (the engine owns it); |
| 41 | +- Expression envelopes (`{ dialect, source }`) → unchanged, no default; |
| 42 | +- a real literal → emitted verbatim, unchanged. |
| 43 | + |
| 44 | +**Existing databases carry the wrong DEFAULT**, so it is corrected through the |
| 45 | +managed schema-drift path (#2186) rather than a bespoke migration: a new |
| 46 | +`default_mismatch` finding with a `drop_column_default` op, categorised `safe` |
| 47 | +(the statement cannot fail and touches no rows). Dev boots with |
| 48 | +`autoMigrate: 'safe'` reconcile it automatically; everywhere else it is reported |
| 49 | +with an actionable hint and applied by `os migrate apply`. Postgres/MySQL use |
| 50 | +`ALTER COLUMN … DROP DEFAULT`; SQLite, which cannot alter a default in place, |
| 51 | +goes through the existing table rebuild — which now re-materialises every |
| 52 | +column's default from **metadata**, so a sibling `defaultValue: 'NOW()'` column |
| 53 | +keeps the default it always had instead of losing it to the rebuild. |
| 54 | + |
| 55 | +**Rows already holding the bogus value are NOT rewritten.** That is #4551's |
| 56 | +standing rule — report, never rewrite — so they stay visible to the |
| 57 | +dangling-reference audit for operators to resolve deliberately. |
0 commit comments