Repository navigation
objectql + cli: backfill the currency code when a stored amount switches to record mode, and a 409 acknowledgement at the metadata door (multi-currency, epic #8345) #27577
Description
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Filed by the epic PM under maintainer ruling 6492000994 on #8345 (letter B, 2026-10-29T08:39Z, 「B 放行,欠显式回填」). Part of #8345. ⛔ Not a claim. Lands after R1 (#27268, #27339, #27338, #27340 merged 2026-10-29) and ⛔ before R2 (#27191 / #27192 / #27193): the R2 home projection has no code to convert a codeless row, so a consolidated sum would silently omit it.
Ruling-ref: 6492000994
维护者速读
- 改了什么: 已有数据的
dynamic/fixed金额字段改成record模式后,旧行没有币种码。新增一个显式回填:运维给定币种码(--code USD),把「有金额、无币种码」的行补上;默认只报告,--apply才写,可重复跑。元数据保存时,这种切换会返回现有的 409destructive_change,说明三个后果和回填命令,force可放行。 - 为什么改: 不回填的话,这些行显示为纯数字、分组落进 null 组、币种筛选里看不到,R2 合并报表也会漏掉它们。
- 风险与回滚: 回填只写币种码为空的行,从不推断币种码;409 不读数据行,不加启动时拒绝。回滚即撤 PR。
- 先看: 先例
packages/objectql/src/summary-backfill.ts与os migrate summary-nulls;packages/metadata-protocol/src/protocol.ts的detectDestructiveObjectChanges(Phase 3a)。 - 两分钟验证: 对一个有旧行的对象把金额切到
record:保存返回 409 并点名命令;dry run 报出待补行数,--apply后再跑一次报 0。 - 需要您做的: 无,已裁 B。
Task
- Backfill function in
@objectstack/objectql, modelled onsummary-backfill.ts. For one object and onerecord-mode amount field, write an operator-supplied ISO 4217 code into every row whose amount is not blank and whose code field (currencyConfig.currencyField) is blank. ⛔ The code is never derived: not from a retireddefaultCurrency, not fromlocalization.currency, not from anything else (ADR-0144 D8 rule 4; [Decision] Currency, all of it: resume multi-currency (#8345) and in what shape, and one rule for an amount whose currency is not known (Studio's USD, the template's USD,fixed's CNY, the example's USD) #27119 Q2 X). A row that already holds a code is never touched; a code that failsiso_4217_currencyis refused before any write. Dry run by default,--applyto write, idempotent (a second run finds nothing), per-row counts: written / skipped (code present) / skipped (amount blank). Nosys_migrationflag: nothing gates on the run. Whether it writes throughengine.updateor at the driver assummary-backfill.tsdoes is the card's call: follow the precedent you pick and say why in the PR. - CLI face
os migrate record-currency-codes --object <o> --field <amount> --code <ISO> [--apply]in@objectstack/cli, the same split assummary-backfill.ts/os migrate summary-nulls. - Metadata-door acknowledgement:
detectDestructiveObjectChanges(packages/metadata-protocol/src/protocol.ts, thesaveMetaItemPhase 3a-destructive gate) gains one finding,currency_mode_to_record: the previouscurrencyConfig.currencyModeis notrecord, the next isrecord, and the field is stored. It answers as the existing HTTP 409destructive_changewith the existingforceremedy. Its message names the three consequences (codeless rows display as a plain number, fall into thenullgroup, sit outside every currency-paired filter) and the command above. It reads no rows, like its siblingsfield_type_changeandfield_required_no_default. ⛔ No boot-time refusal: a code-declared package registered at boot has no previous version to diff. - Tests: the backfill (dry run, apply, idempotence, a row with a code untouched, a blank amount skipped, a bad code refused); the 409 finding (fires on dynamic→record and fixed→record over a stored field, not on a new field, lifted by
force); a CLI smoke.
Out of this card's diff, kept with the epic PM (governed paths, Tier H, not mixed into a code PR): the one-line skills/** note (switching a stored field to record leaves rows codeless; run the backfill) and the ADR-0144 open item with its dated note. The epic PM also adds to #27193 that blank-code rows are reported as their own class, apart from missing-rate rows.
Lane: server (objectql, metadata-protocol), with the packages/cli command as the cross-lane half, dispatched by the epic PM per 「epic的卡你统一派」 · Size: M · Review: default. Declarations, for the card to confirm against the tree: Clause-②: yes (widening) (a new member of an existing refusal face, the reading of #27339's review 6490309706) · Metadata-protocol: none (what an author can write is unchanged; force lifts the acknowledgement). Left open on purpose: how a cloud tenant without a CLI reaches the backfill (an admin route or Studio action, M3 or later).
Epic: #8345. Dedupe: #8345 thread, ruling 6492000994, #27193 (R2 recompute: a different operation on the home columns); no open card carries this slice.
Activity
objectstack-fleet commentedon Oct 29, 2026
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Claim: PM loop round 1 · 2026-10-29T13:42Z
Session:
session_01Utp6CbTZrFqRYNgjSaSbN5(epic PM,epic:#8345)Account:
os-billBranch:
claude/project-thread-mzbmzo(card threadsession_01Utp6CbJa6hKdimkAau1nbA; the platform assigns the thread's one push branch — the recorded platform exception)Worktree:
objectstack-issue-27577Domain:
domain:serverSeat:
epic:#8345File surface:
packages/objectql/src/**(the backfill and its tests),packages/cli/src/commands/migrate/**(the thin command, cross-lane half),packages/metadata-protocol/src/protocol.tsand its tests,.changeset/*; ⛔ no governed path (stop on breach; explain in the report)Container & model:
M,mode:thread,model: opus (TIER_DEFAULT; dispatch-gates --tier: no path-derived mandate),effort: highClause-②: yes (widening) — to confirm against the tree
Metadata-protocol: none — to confirm against the tree
Responsibility: n/a — not a defect card
Thread-read: none
Premise: holds
Serial constraints cleared: yes — R1 (#27268, #27339, #27338, #27340) landed 2026-10-29; ⛔ lands before R2 (#27191–#27193).
Release pairing: none — ships in the next Version Packages.
Maintainer authority: Bill, project thread, 2026-10-28 「epic的卡你统一派」, 「并发可以5」, 「你自主完成后续阶段」; ruling 6492000994 (letter B).