You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge origin/main — reconcile generateSql with #3652's rendering
#3652 rewrote `ObjectQLStrategy.generateSql` (date_trunc for bucketed
dimensions, COUNT DISTINCT, ORDER BY/LIMIT/OFFSET) and made `execute()`
echo the rendered SQL on the result. Kept all of that.
Resolved the WHERE clause in favour of this branch's version, which
renders the same caller filters plus the two things #3652's inline loop
does not:
- the READ SCOPE (`compileScopedFilterToSql`), the point of #3602
residual 2 — without it the echoed string still reads as an unscoped
table scan while the aggregate is scoped;
- `assertJoinedScopesEnforceable`, so it never renders SQL for a query
`execute()` would reject.
Its filter rendering is also closer to what runs: `contains` binds
`%v%` (a bare comparand under LIKE would only match exactly), and
values go through `coerceFilterValueForObjectQL`, so the comparand
shown is the one this path hands the engine.
Scope VALUES stay in `params`, which the `execute()` echo discards —
only the predicate shape reaches the browser, same as NativeSQL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013st2KArjmLQSuVzpDS91jj
Copy file name to clipboardExpand all lines: content/docs/deployment/cli.mdx
+42Lines changed: 42 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -517,6 +517,48 @@ first. It never drops a table that is absent from your metadata, and on SQLite
517
517
it reconciles via a table rebuild (copy → swap) that preserves your data.
518
518
</Callout>
519
519
520
+
#### Data migrations
521
+
522
+
The commands above reconcile **schema**. A *data* migration rewrites rows, and
523
+
whether it is done is a fact about **your** database, not about the platform
524
+
version you installed — so each deployment runs it, and its result is recorded
525
+
where the data lives.
526
+
527
+
| Command | Description |
528
+
|---------|-------------|
529
+
|`os migrate files-to-references`| Convert legacy file-field values to `sys_file` references, verify the ownership ledger, and record the deployment's migration flag |
530
+
531
+
```bash
532
+
os migrate files-to-references # Dry run: full report, writes nothing
533
+
os migrate files-to-references --apply # Convert, verify, record the flag (prompts)
534
+
os migrate files-to-references --apply --yes --json # CI / scripts
535
+
os migrate files-to-references --object product # Restrict to one object (repeatable)
536
+
```
537
+
538
+
A `file` / `image` / `avatar` / `video` / `audio` field value is an opaque
539
+
`sys_file` id that the platform owns. Values written before that (an inline
540
+
`{url, name, …}` blob, or a URL naming this platform's own
541
+
`…/storage/files/:id` resolver) are converted in place; **external URLs are
542
+
reported, never re-hosted** — re-hosting third-party content is a licensing and
543
+
privacy decision, and the right fix is usually to model the field as a `url`
544
+
field instead.
545
+
546
+
The run then reconciles what records actually hold against what `sys_file`
547
+
records as each file's owner. Zero blocking discrepancies is what records the
548
+
flag — and that flag, not the version number, is what later enables features
549
+
that act irreversibly on migrated data (reclaiming the bytes of released files;
550
+
rejecting rather than warning about legacy media values). Never running it is
551
+
safe: files are simply retained forever.
552
+
553
+
Exit status is `0` only when the self-check passes, so CI can gate on it.
554
+
555
+
<Callouttype="warn">
556
+
A dry run writes **nothing** — not the conversions, and not the flag either,
557
+
even when the self-check would pass. `--apply` is the only writing mode. A
558
+
later run that *fails* its self-check clears the flag's verified state, so a
|**object**|`{ name: string; label?: string; pluralLabel?: string; description?: string; … }`| ✅ | Full object definition to create |
70
92
71
93
94
+
---
95
+
96
+
## DataMigrationFlag
97
+
98
+
Deployment-level record that a data migration ran here and its self-check passed — the evidence gate consumers read instead of the platform version
99
+
100
+
### Properties
101
+
102
+
| Property | Type | Required | Description |
103
+
| :--- | :--- | :--- | :--- |
104
+
|**id**|`string`| ✅ | Migration id (e.g. adr-0104-file-references) — one row per data migration |
105
+
|**last_run_at**|`string`| ✅ | When this migration last completed a gated (apply-mode) run on this deployment |
106
+
|**verified_at**|`string \| null`| optional | When the self-check last PASSED. Null/absent until it does — and cleared again by a later failing run, so a regression closes the gate |
107
+
|**applied_at**|`string \| null`| optional | When the backfill last ran in apply mode (writes enabled) |
108
+
|**blocking**|`integer`| ✅ | Blocking discrepancies reported by the last self-check. The gate requires 0 |
109
+
|**advisory**|`integer`| optional | Advisory findings from the last run (external URLs, stale owners, …) — cost storage or need a modelling decision, never block the gate |
110
+
|**details**|`string`| optional | JSON-encoded counts from the last run, for diagnostics |
0 commit comments