Repository navigation
Conversation
…fiers
ri_exchange_history.account_id has been VARCHAR(20) with
CHECK (account_id ~ '^\d{12}$') since 000009: the exact shape of an AWS
account ID. An Azure subscription GUID overflows the width (22001), and
any non-AWS value, including a GCP project ID that fits in 20 characters,
is rejected by the CHECK (23514) even at VARCHAR(255). Latent today (only
the AWS exchange path writes this table), but it would drop the audit row
of an already-billed exchange as soon as an Azure or GCP writer lands.
Migration 000137 widens the column to VARCHAR(255) (matching
cloud_accounts.external_id and purchase_history.account_id) and drops the
AWS-only CHECK. Each half is conditioned on the catalog state, so fresh,
deployed, partially fixed and re-run databases all converge, and both are
re-checked afterwards. The table lock is taken before any catalog read so
a concurrent replay waits and then no-ops instead of failing with
"constraint does not exist". The down migration refuses while any
non-AWS row exists, then narrows and restores the CHECK.
The AWS shape is now enforced in Go at the write boundary that knows the
provider: the AWS exchange store adapter refuses any account ID that is
not 12 digits before calling the store, so AWS rows keep the guarantee
the CHECK gave them, including refusing the "unknown" placeholder
resolveAccountID returns when STS fails.
Closes LeanerCloud#144
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
|
Independent review 2 (claude-fleet, cold): APPROVE 452e2df Migration order. This PR adds 000137, and the highest migration on origin/main is 000133. Migrations merge strictly in number order:
The PR is based on a main from before 131, but Up and down. They are symmetric on the normal path:
Findings (none blocking):
Attacked:
Verified locally, on a
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
VERDICT: APPROVE 452e2df PR #1170 (draft, @IAmJSD), fix(db): let ri_exchange_history.account_id hold Azure and GCP identifiers. Closes #144 (open). Migration number: origin/main's latest migration is 000133. 000137 does not collide with main or with any open PR:
Main has had gaps before (60->63, 67->70, 81->83, 83->86), and Findings
Checked
Mutation probes (scratch export, each restored and verified with cmp)
Local runs on the exact head (macOS, Docker)
|
Root cause
ri_exchange_history.account_idhas beenVARCHAR(20) NOT NULL CHECK (account_id ~ '^\d{12}$')since 000009, which is the exact shape of an AWS account ID. Two separate barriers block any other cloud:VARCHAR(20)(SQLSTATE22001).23514). That includes a GCP project ID short enough to fit 20 characters, and an Azure GUID even after widening.No migration has touched either barrier since 000009. Today this is latent: only the AWS exchange path writes this table. The first Azure or GCP writer would hit it and lose the audit row of an exchange that has already been billed, the same failure as the sibling
purchase_historycolumn (cloud-commitments-cli#1603, fixed by 000095).Fix
Migration 000137 (
internal/database/postgres/migrations/000137_ri_exchange_history_account_id_multicloud.{up,down}.sql)account_idtoVARCHAR(255), matchingcloud_accounts.external_id,savings_snapshots.account_idandpurchase_history.account_id. It leaves unboundedvarcharand anything wider than 255 alone.account_idthat contains the^\d{12}$regex. No replacement CHECK is added: AWS account IDs, Azure GUIDs and GCP project IDs share no shape, and the table has no provider column a CHECK could key on.purchase_history.account_idalso has no CHECK.regclass, the way 000095 does it. So fresh, deployed, re-run and partially fixed databases all converge, whether the column is widened but still CHECKed, unCHECKed but still narrow, or carries the CHECK under another name. A missing column is a hard error, and both halves are read back from the catalog afterwards, so the migration can't be recorded as applied without the work done.LOCK TABLE ... ACCESS EXCLUSIVEbefore reading the catalog. Without it, a second run that starts while the first is uncommitted (for example auto-heal or a manual replay outside golang-migrate's advisory lock) reads the stale catalog, waits on the first run's lock, and then fails withconstraint "ri_exchange_history_account_id_check" ... does not exist (SQLSTATE 42704). With the lock it waits and then does nothing. Widening a varchar is binary-coercible, so there is no table rewrite and no rebuild ofidx_ri_exchange_history_account.account_idisn't 12 digits, because narrowing would truncate or drop exchange audit rows. Otherwise it narrows from exactlyVARCHAR(255)and restores the CHECK under its original name.Go boundary check (
internal/server/handler_ri_exchange.go)Dropping the CHECK alone would quietly change live AWS behavior. When STS fails,
resolveAccountIDreturns the placeholder"unknown", and today the CHECK rejects that row.configExchangeStoreAdapter, the only writer of this table, is AWS-only, so itsSaveRIExchangeRecordandReserveRIExchangenow refuse any account ID that isn't 12 ASCII digits before calling the store. AWS rows keep exactly the guarantee the CHECK gave them, and the check now sits at the boundary that knows the provider, as the issue suggests. Five mock test fixtures that used a 9-digit"123456789"(plus one with an empty ID) were updated to"123456789012".Closes #144
Verification
All evidence comes from local unit tests plus integration tests on a real Postgres testcontainer (macOS, Docker). Nothing touched a real cloud account. Tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid.Fail-before.
TestMigration137_RIExchangeHistoryAccountIDMultiCloudpins the schema atpreviousMigrationVersion(t, 137)and asserts the bug there: an Azure GUID insert fails with22001, andcudly-prod-0042(fits the width) fails with23514. After 000137, AWS, Azure, 30-character GCP and short GCP IDs all insert and round-trip. A re-run of the up migration is a no-op.Mutation probes. Each one was applied to the migration or adapter, run, then reverted:
LOCK TABLEin upTestMigration137_ConcurrentRunsSerialize/from_widened_but_still_checkedwith SQLSTATE 42704TestMigration137_DownRefusesNonAWSRowscasesTestMigration137_DownRestoresAWSConstraintWhenSafeTestConfigExchangeStoreAdapter_RefusesNonAWSAccountIDThe concurrency test is deterministic. Run 1 executes the up migration inside an open transaction. Run 2 starts and is confirmed blocked through
pg_locks. Run 1 commits, and run 2 must then succeed.One note on the refusal assertion: the migrate error embeds the whole SQL file, so the test matches the formatted
1 row(s) hold ...text, which only the raised message contains. A plain message-literal assertion passed even with the guard removed.Commands:
Three
TestPostgresStoreDB_ReserveApprovedRIExchange_*tests skip locally because they need a dedicatedRI_EXCHANGE_TEST_DATABASE_URL. That skip predates this PR.Out of scope (left for follow-ups)
ri_exchange_history.cloud_account_idis never populated or projected, so exchange-history scoping still matches on the provider account string. Not changed here.ri_exchange_historyrow and has no daily cap. This PR only removes the schema barrier that ledger write would hit. It does not add an Azure writer, and it does not relax the AWS adapter, which stays 12-digit-only. A future Azure or GCP writer needs its own provider-aware validation at its own boundary.Maintainers: please mirror the issue's triage labels onto this PR:
triaged,priority/p3,severity/low,urgency/eventually,impact/few,effort/s,type/bug.🤖 Generated with Claude Code