Task 3 correction: migration 0003 unconditionally seeds a profile row on every fresh install, and Task 1's migration 0023 preserves this seeded row with user_id = NULL. Therefore, a fresh install always has at least one profile row with user_id IS NULL when users table is empty -- the "no-op on genuinely fresh install" scenario was unreachable dead code. Confirmed with the codebase owner that no scenario requires defending against a missing profile row. Collapse the two-branch error handling into a single `if err != nil` check (matching the pattern used elsewhere in the function), remove the now-unused database/sql import, update the function's doc comment to remove the false claim about fresh installs, and delete the now-unreachable TestClaimLegacyOwner_NoOpOnGenuinelyFreshInstall test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
1.8 KiB
Go
55 lines
1.8 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
)
|
|
|
|
// ClaimLegacyOwner is a one-time upgrade step, not a normal runtime
|
|
// operation: it binds every pre-multi-tenancy row (user_id IS NULL, left
|
|
// that way by migrations that can't take runtime parameters -- see
|
|
// docs/superpowers/specs/2026-07-25-per-user-profile-design.md) to a single
|
|
// new user identified by oidcSub. Safe to call on every startup: once the
|
|
// users table is non-empty, it's a no-op, so leaving
|
|
// GENIUSRUN_LEGACY_OWNER_OIDC_SUB set after the first successful run causes
|
|
// no harm.
|
|
func (db *DB) ClaimLegacyOwner(ctx context.Context, oidcSub string) error {
|
|
var userCount int
|
|
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&userCount); err != nil {
|
|
return fmt.Errorf("count users: %w", err)
|
|
}
|
|
if userCount > 0 {
|
|
return nil // already bootstrapped (either claimed already, or real signups exist)
|
|
}
|
|
|
|
var displayName string
|
|
err := db.QueryRowContext(ctx, `SELECT name FROM profile WHERE user_id IS NULL LIMIT 1`).Scan(&displayName)
|
|
if err != nil {
|
|
return fmt.Errorf("find legacy profile: %w", err)
|
|
}
|
|
|
|
tx, err := db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin claim legacy owner tx: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
res, err := tx.ExecContext(ctx, `INSERT INTO users (oidc_sub, display_name) VALUES (?, ?)`, oidcSub, displayName)
|
|
if err != nil {
|
|
return fmt.Errorf("create legacy owner user: %w", err)
|
|
}
|
|
userID, err := res.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// table is always one of the fixed literals below, never user input.
|
|
for _, table := range []string{"profile", "workout_kinds", "activities", "sync_state", "sync_runs"} {
|
|
if _, err := tx.ExecContext(ctx, `UPDATE `+table+` SET user_id = ? WHERE user_id IS NULL`, userID); err != nil {
|
|
return fmt.Errorf("claim legacy %s rows: %w", table, err)
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|