Repository navigation
fix(db): remove ReplaceRecommendations and its unscoped DELETE - #1209
Conversation
ReplaceRecommendations ran `DELETE FROM recommendations` with no WHERE and then reinserted, so a caller handing it a partial or empty snapshot would wipe every account's recommendations. It had no production caller: the only non-test reference was the ConfigStore interface. Remove it from the store, the interface and every mock and fake. It was the only caller of the batch insert with onConflict=false, so drop that parameter and make the natural-key ON CONFLICT clause unconditional. Tests that only exercised the removed function are deleted. Those that used it for seeding or resetting now seed through UpsertRecommendations or wipe the table with a test-only helper. UpsertRecommendations keeps the coverage for clearing last_collection_error and the partial marker. Add TestNoUnscopedRecommendationsDelete, which parses every non-test Go file under internal/ and cmd/ and fails on a DELETE FROM recommendations literal with no WHERE, plus a matcher self-test. Closes LeanerCloud#159 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review 2 (claude-fleet, cold): APPROVE e9350f8 Against #159 (option 1):
Findings (none blocking):
Attacked:
Verified locally, with no cloud credentials, under the heavy lock, with testcontainers Postgres:
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
Resolve the conflict in internal/config/store_postgres_recommendations_test.go with LeanerCloud#1165 (shared migrated Postgres container): keep main's setupRecommendationsStore on testhelpers.RequireMigratedDatabase and drop TestPostgresStore_ReplaceRecommendations, which this branch removes along with the method. The wipeRecommendations helper stays; its comment now says subtests share one test database rather than one container. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of LeanerCloud#1209 showed the guard missed `DELETE FROM public.recommendations`, `TRUNCATE recommendations` and a WHERE that only appears in a SQL comment. Strip SQL comments before looking for WHERE, flag TRUNCATE of the table (including in a table list), accept schema-qualified and quoted names, and unquote the Go literal before matching. The matcher self-test gains a row for each form plus negatives for recommendations_state. Document that the guard is a best-effort tripwire: SQL assembled from separate literals gets past it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged origin/main to resolve the conflict in Conflict:
Findings
New head: 7bb8719 (merge 7374a26 plus the guard commit) Verification on the merged tree (Linux, no cloud credentials,
The PR is still a draft. |
Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
VERDICT: APPROVE d1d93f9 PR #1209 (closes #159), head d1d93f9, reviewed against origin/main 6776e27. Findings: none blocking.
Attacked:
|
|
Independent review 2 (claude-fleet, cold), re-review: APPROVE d1d93f9 Delta (e9350f8..d1d93f9, first-parent): 7374a26 is a merge with a hand resolution in internal/config/store_postgres_recommendations_test.go. 7bb8719 is test-only (+45/-8, guard). d1d93f9 is a clean merge. Findings:
Attacked:
Verified locally (merged tree 2c19f9a, GOTOOLCHAIN=go1.26.10, no credentials, heavy lock):
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
Local verification at
CI on this head: CI Success and pre-commit green. cristim's independent review approved this head. Migrations in this batch (000136-000139) are applied in numeric order. Nothing was run against any cloud. |
|
Marked ready and merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), after cristim's independent review approving |
Bring the branch up to date with main. Conflicted files: - internal/config/store_postgres_recommendations.go: main (LeanerCloud#1209) removed ReplaceRecommendations and the onConflict flag, making the natural-key ON CONFLICT clause unconditional. The branch had switched that clause to the 12-column key with az_config. Kept main's unconditional upsert and applied the branch's az_config arbiter to it, plus the branch's doc comment on the 12-column index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the branch up to date with main. Conflicted files: - internal/analytics/collector_test.go: main kept adding stubs to the hand-rolled analytics mockConfigStore (HistorySummaryAggregate from LeanerCloud#1200, ClaimCollectionInvocation from LeanerCloud#1099, the ReplaceRecommendations removal from LeanerCloud#1209, and the pgx import they need). This branch replaces that hand-rolled mock with one that embeds mocks.MockConfigStore, which already carries all of those methods on main. Kept the branch's version; main's stub edits are covered by the shared mock and drop out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
ReplaceRecommendations(internal/config/store_postgres_recommendations.go) ranDELETE FROM recommendationswith noWHEREon provider, account or collection time, then reinserted. Any caller handing it a partial or empty snapshot would have wiped every account's recommendations, not just its own. It had no production caller: the only non-test reference was theConfigStoreinterface declaration.Fix
This takes the issue's preferred option 1 and removes the function.
Before removing it I checked origin/main for any caller:
ReplaceRecommendations(open and all states) return nothing that depends on it.Changes:
ReplaceRecommendationsis removed. It was the only caller of the batch insert withonConflict=false, so that parameter is gone too and the natural-keyON CONFLICT ... DO UPDATEclause is now unconditional. That is exactly whatUpsertRecommendationsalready sent; leaving a parameter that is alwaystruewould also tripunparam.ConfigStore(internal/config/interfaces.go, with its comments updated),internal/mocks/stores.go,internal/server/test_helpers_test.goandinternal/analytics/collector_test.go.TestPostgresStore_ReplaceRecommendationsand theReplaceRecommendations stores the same AZ pairsubtest ofTestPostgresStore_UpsertRecommendations_MultiAZBatch.TestPostgresRecommendationSourceEncodingRollback. The Upsert half of each still runs.UpsertRecommendations(..., nil).wipeRecommendationshelper.last_collection_errorand the partial marker:TestPostgresStore_CollectionPartial_RecordedAndClearedstill covers this forUpsertRecommendations. It sets an error and a partial marker, upserts, and asserts both are nil. Only the Replace half of that test was removed.internal/config/recommendations_scoped_delete_guard_test.goaddsTestNoUnscopedRecommendationsDelete.internal/andcmd/, and fails on any string literal that deletes fromrecommendationswithout aWHEREin the same literal (SQL comments are stripped first), or that truncates it. Schema-qualified and quoted table names count.TestUnscopedRecommendationsDeletesMatcher, checks that the matcher flags the unscoped, lowercase, schema-qualified, quoted, WHERE-only-in-a-comment and TRUNCATE forms, and ignores scoped deletes andrecommendations_state."DELETE FROM " + table) gets past it.internal/api/purchase_pricing.goand its test now refer to the refresh's stale-row eviction.Concurrency: this PR changes no write path's behavior. It removes a function and inlines a constant
true, so I found no new race to test.Verification
All evidence is local unit, mock and real-Postgres (testcontainers) on macOS. Tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/nullbecause this machine's default AWS profile is invalid.The guard fails before the fix and passes after. With origin/main's
store_postgres_recommendations.goswapped back in:With the fix, both tests pass.
Build, vet and tests:
go build ./...passes.go vetpasses on config, mocks, server, analytics and api, and again with-tags integrationon config and api.go test -count=1passes for each package, run one at a time:./internal/config/,./internal/mocks/(the repo-wide guards),./internal/analytics/,./internal/server/,./internal/api/and./internal/scheduler/.go test -tags integration ./internal/config/ -run 'TestPostgresStore_UpsertRecommendations|TestPostgresStore_ListStoredRecommendations|TestPostgresStore_Freshness|TestPostgresStore_CollectionPartial|TestPostgresRecommendationSourceEncodingRollback|TestTxRollbackFailureIsLogged|TestNoUnscoped|TestUnscoped'passes (17 top-level tests).go test -tags integration ./internal/api/ -run TestReshapeRecommendations_Integrationpasses (4 tests).Other checks:
--allow-parallel-runners, with and without--build-tags integration, reports 0 issues.gocyclo -over 10 -ignore "_test\.go"on the changed non-test files is silent.git inittemp repo with an isolated HOME, using--register-awsplus the patterns fromscripts/setup-git-secrets.shand the repo's.gitallowed. The scan is clean, and a planted account ID was caught.gofmt -lis clean andgit diff --checkfinds no whitespace errors.Not covered: nothing here needs real cloud credentials. The change is DB-layer only, and nothing was deployed.
Out of scope / notes
internal/database/postgres/migrations/000129_recommendations_state_partial.up.sqlstill says "UpsertRecommendations/ReplaceRecommendations reset it to NULL". I didn't edit it because the migration is already applied.last_collection_errordesign is unchanged: it is still a single global row (recommendations_state WHERE id = 1). As the issue says, that's a separate schema/UX decision.internal/config/store_postgres_recommendations_test.go, is resolved. The tests use main'sRequireMigratedDatabase, andTestPostgresStore_ReplaceRecommendationsis dropped along with the method.Could a maintainer please mirror the issue's triage labels onto this PR? They are
impact/internal,triaged,priority/p3,severity/low,urgency/eventually,effort/xsandtype/bug.Closes #159
🤖 Generated with Claude Code