Repository navigation
Conversation
The direct-execute, ambient (host-account) and legacy single-account paths reach SavePurchaseHistory with no CloudAccountID, so their rows were stored with cloud_account_id NULL even after the 000139 backfill. SavePurchaseHistory now resolves a missing cloud_account_id in the INSERT from (provider, account_id) with the backfill's rules: lower-cased provider, exact account id, never an empty id. The lookup locks the account FOR KEY SHARE so a concurrent account delete makes the row NULL instead of failing the insert on the foreign key. A row that matches no registered account is still stored (dropping it would be an audit gap, LeanerCloud#621) and logged as a warning. The function moves to its own file out of store_postgres.go. The marketplace constraint test's "missing account" rows now clear the resolved account to keep a legacy unlinked row, and a new case covers a host-account row the save links to its registered account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aces Real-Postgres tests for LeanerCloud#1235: rows shaped like each write path get their account, an explicit id wins, rows that match nothing are kept with NULL, the dedupe path does not rewrite a row, and a save racing an account delete stores NULL instead of failing on the foreign key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
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 |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
Every purchase_history row is written by one Go function:
PostgresStore.SavePurchaseHistory, called only frompurchase.Manager.savePurchaseHistory. That function storedexec.CloudAccountIDas given. Only the multi-account fan-out sets it. On the single-account path it is nil:PlanID == "", and the account is taken from the recs;On all three,
resolveSingleAccountProviderresolves the account and stamps itsExternalIDonAccountID, but nothing put the UUID oncloud_account_id. So new NULL rows kept appearing after the 000139 backfill (#1201).Fix
SavePurchaseHistorynow fills a missingcloud_account_idinside the INSERT itself. It usesCOALESCE($17::uuid, (SELECT id FROM cloud_accounts ... FOR KEY SHARE)), which:(provider, external_id), the cloud_accounts UNIQUE key;AWS);CloudAccountIDfrom the caller, as before;Concurrency. The lookup locks the account
FOR KEY SHARE.ON DELETE SET NULLwould leave."Fail loudly". I did not make an unresolvable account refuse the row.
SingleCloudAccountIDFromRecsreturns nil for them). Refusing would hide every purchase they make.Warnfnames the provider and account id. Readers still match it by the externalaccount_id.If you'd rather refuse, say so: it's a one-line change. The NOT NULL constraint the issue mentions should wait until it's clear that ambient-only deployments can't produce unlinked rows.
SavePurchaseHistorymoved out of the 4.5k-linestore_postgres.gointointernal/config/store_postgres_history_save.go. The SQL now usesQueryRow ... RETURNING cloud_account_id IS NOT NULLso it knows when to warn.pgx.ErrNoRowskeeps the old "already recorded" path.Behavior change worth a look
New host-account rows written by the ambient path are now linked to the registered account with the same external id. That makes them visible to account-scoped permission checks:
sellgrant now matches such a row;The 000139 backfill does the same for existing rows, and the marketplace path already resolved such rows to that account for signing (#560).
TestMarketplacePermissionConstraintsHTTP:missing-account/own-missing-accountcases now clear the resolved id after the save, so they still test a legacy unlinked row;resolved-accountcase pins the new behavior.Verification
All on macOS against real Postgres 16 via testcontainers. I ran the tests with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid. No cloud APIs were called.Fail-before (base
c8bdd460plus only the new test file):Pass-after:
go test -tags integration -count=1 ./internal/config/: okgo test -tags integration -count=1 ./internal/api/: ok (273s, whole package)go test -tags integration -count=1 -run '^TestMigration095_' ./internal/database/postgres/migrations/: okgo test -count=1on./internal/config/ ./internal/mocks/ ./internal/purchase/ ./internal/api/: okgo test -short -race -count=1 ./internal/config/: okMutation probes (scratch edits of the SQL, each restored; the final file is byte-identical). All were caught:
FOR KEY SHAREConcurrentAccountDelete,ConcurrentSavesAndDelete(FK violation)LOWER()legacy_upper-case_providerambient_aws_host_account,azure_row_with_the_shared_number,legacy_upper-case_provider$1 <> ''empty_account_idan_explicit_cloud_account_id_is_kept,TestPGXMock_..._ResolvesAccountAndKeepsUnmatchedRow$17only)subscription_differing_only_in_caseStatic checks:
go build ./...,go vet(with and without-tags integration) andgofmt: clean.golangci-lint v2.10.1 run --build-tags integration ./internal/config/... ./internal/api/...: 0 issues.gocyclo -over 10on the changed non-test files: silent.scripts/gosec-hook.sh, v2.28.0): exit 0.--register-awsas in CI, isolated HOME, fresh repo with the changed filesgit added):scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Coverage gaps:
pg_stat_activity. There is also a 20-way concurrent smoke test.Out of scope / notes
account.ID. If that account is deleted mid-purchase, the insert can still fail on the FK, which is an audit gap. I kept explicit ids FK-enforced rather than silently nulling a bogus id. Worth a separate issue if you want it closed too.Overlap with open PRs
SavePurchaseHistorywithCloudAccountID: niland a registeredaccount_id. After this PR, those rows get linked automatically. If the fixtures need truly unlinked rows, they need the sameUPDATE ... SET cloud_account_id = NULLused here, or a raw INSERT.SavePurchaseHistorywith no matching registered account (or with an explicit id), so no behavior change is expected.handler_purchases.goandpurchase/*, not this code, so no conflict.Frontend follow-up for the UI owner
None needed. History already matches rows by either id.
Closes #1235
Maintainers: please mirror the issue's triage labels on this PR:
priority/p2,severity/medium,urgency/this-quarter,impact/few,effort/s,type/bug,triaged.🤖 Generated with Claude Code