Skip to content

fix(etl): persist every user field the create contract accepts - #466

Merged
rickyrombo merged 2 commits into
fix/etl-user-create-profile-typefrom
fix/user-create-field-parity
Aug 7, 2026
Merged

fix(etl): persist every user field the create contract accepts#466
rickyrombo merged 2 commits into
fix/etl-user-create-profile-typefrom
fix/user-create-field-parity

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

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's create_user_request_body accepts four fields the ETL create path was dropping, and the API serves all four from get_users — so a signup that set any of them lost it until the user next edited their profile:

Field In API create body Read on create before
profile_type yes no → #458
allow_ai_attribution yes no
spl_usdc_payout_wallet yes no
playlist_library yes no
coin_flair_mint no* no

* 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 by 0175, between 0141 (profile_type) and 0200 (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_id references 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 reads is_deactivated = false.

Why this keeps happening

users is the only entity whose create and update paths keep independently hand-written column lists. Tracks funnel both through insertTrackAndRouteWithState; playlists share a playlistRow. For those a new column lands in both automatically. For users it's two edits, and nothing enforced the second.

Two guards

TestUserCreateAndUpdateCoverSameColumns diffs the INSERT and UPDATE column lists and fails on any column the update writes and the create drops. Exceptions must carry a reason. It found coin_flair_mint the first time it ran.

insertedUserValue looks 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

  • The 29-parameter INSERT PREPAREs against a live database (validates every column name and type)
  • Placeholder/argument alignment checked: max=$29 distinct=29 args=29
  • Full pkg/etl suite passes

🤖 Generated with Claude Code

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>
@rickyrombo
rickyrombo merged commit db5c632 into main Aug 7, 2026
3 checks passed
@rickyrombo
rickyrombo deleted the fix/user-create-field-parity branch August 7, 2026 09:00
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant