fix(etl): persist every user field the create contract accepts - #466
Merged
rickyrombo merged 2 commits intoAug 7, 2026
Merged
Conversation
Builds on #458. That PR fixed profile_type; the same miss covers three more fields, and the reason it keeps happening is structural. users is the only entity whose create and update paths keep independently hand-written column lists -- tracks funnel both through insertTrackAndRouteWithState, playlists through a shared playlistRow, so a new column lands in both automatically. For users it is two edits and nothing enforces the second. Four fields have now been added to UPDATE and missed on INSERT: playlist_library, allow_ai_attribution, spl_usdc_payout_wallet and profile_type. api's create_user_request_body accepts all of them, so a signup that sets any one silently lost it until the user next edited their profile, and the API serves all four from get_users. They are now read on create. artist_pick_track_id stays update-only, and that is not a schema accident: it references a track the account cannot own at signup. Across 292,111 users never modified after creation it appears zero times. is_deactivated likewise -- creating an already-deactivated account is meaningless, and the create path's own existence check reads is_deactivated = false. Two guards so this class fails loudly instead of silently: TestUserCreateAndUpdateCoverSameColumns diffs the INSERT and UPDATE column lists and fails on any column the update writes and the create drops, with an exception list that has to carry a reason. It found coin_flair_mint immediately. insertedUserValue looks arguments up by column name. The old tests asserted on argument offsets -- "the three state flags are the trailing arguments" -- so adding a column broke them in a way that read as a logic failure rather than a moved index. Position no longer matters. Verified the 29-parameter INSERT prepares against a live database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new nullStrPtrFromMeta helper landed between insertUser's doc comment and its declaration, so the comment about new accounts being unverified, active and available read as documentation for the helper instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 7, 2026
rickyrombo
added a commit
to AudiusProject/api
that referenced
this pull request
Aug 8, 2026
## The gap `update_user_request_body` has carried `coin_flair_mint` since `0175`. The create body never did. Nothing about the field explains the difference. The column comment describes it as: > the coin which the user has selected as their preferred flair. NULL for auto, empty string for none. That's a display preference — the same shape as `spl_usdc_payout_wallet`, which *is* accepted on create. The ordering makes oversight the likelier reading than intent: | Migration | Field | In create body? | |---|---|:--:| | `0141` | `profile_type` | yes | | `0175` | `coin_flair_mint` | **no** | | `0200` | `spl_usdc_payout_wallet` | yes | The one in the middle is the only one missing. ## Result Create and update now differ by exactly the fields that should differ: ``` create-only : user_id, wallet update-only : artist_pick_track_id, is_deactivated ``` Both update-only fields have reasons independent of any schema: - **`artist_pick_track_id`** references a track the account cannot own at signup. Across 292,111 users never modified after creation, it appears **zero** times. - **`is_deactivated`** — creating an already-deactivated account is meaningless. ## Verification Parsed the modified document with `js-yaml`: ``` YAML parses OK create has coin_flair_mint: true create fields: 21 update fields: 21 update-only: is_deactivated, artist_pick_track_id ``` Three lines, response schemas untouched. ## Context Found while auditing the indexer, which was dropping four fields the create body already accepted — `profile_type`, `allow_ai_attribution`, `spl_usdc_payout_wallet`, `playlist_library`. Fixed in OpenAudio/go-openaudio#466, which also adds a test pinning the indexer's create and update column sets together. This PR closes the same gap one layer up so all three layers — API contract, SDK schema, indexer — agree. The SDK's Zod `CreateUserSchema` is a separate hand-written contract that's also missing `coin_flair_mint` (and `profile_type`); worth a follow-up there. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Stacked on #458 (based on
fix/etl-user-create-profile-type, so it merges cleanly after it).Same miss, three more fields
#458 fixed
profile_type.api'screate_user_request_bodyaccepts four fields the ETL create path was dropping, and the API serves all four fromget_users— so a signup that set any of them lost it until the user next edited their profile:profile_typeallow_ai_attributionspl_usdc_payout_walletplaylist_librarycoin_flair_mint* not in the create body, but the column comment describes it as "the coin which the user has selected as their preferred flair" — a display preference, structurally identical to
spl_usdc_payout_wallet. It was added by0175, between0141(profile_type) and0200(spl_usdc_payout_wallet), both of which are in the create body. Its absence looks like the same oversight one layer up rather than intent. Reading it here is harmless; the matching API change is filed separately.What stays update-only, and why
Not because a schema omits them — because they cannot be meaningful at signup:
artist_pick_track_idreferences a track the account cannot own yet. Across 292,111 users never modified after creation it appears zero times.is_deactivated— creating an already-deactivated account is meaningless, and the create path's own existence check readsis_deactivated = false.Why this keeps happening
usersis the only entity whose create and update paths keep independently hand-written column lists. Tracks funnel both throughinsertTrackAndRouteWithState; playlists share aplaylistRow. For those a new column lands in both automatically. For users it's two edits, and nothing enforced the second.Two guards
TestUserCreateAndUpdateCoverSameColumnsdiffs the INSERT and UPDATE column lists and fails on any column the update writes and the create drops. Exceptions must carry a reason. It foundcoin_flair_mintthe first time it ran.insertedUserValuelooks arguments up by column name. The previous tests asserted on argument offsets — "the three state flags are the trailing arguments" — so adding a column broke them in a way that read as a logic failure rather than a moved index. That happened while writing this PR, which is why the helper exists.Verification
PREPAREs against a live database (validates every column name and type)max=$29 distinct=29 args=29pkg/etlsuite passes🤖 Generated with Claude Code