package postgres import ( "context" "fmt" "strings" "git.gobha.me/xcaliber/chat-switchboard/models" ) type UserModelSettingsStore struct{} func NewUserModelSettingsStore() *UserModelSettingsStore { return &UserModelSettingsStore{} } func (s *UserModelSettingsStore) GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error) { rows, err := DB.QueryContext(ctx, ` SELECT id, user_id, model_id, hidden, preferred_temperature, preferred_max_tokens, sort_order, created_at, updated_at FROM user_model_settings WHERE user_id = $1 ORDER BY sort_order, model_id`, userID) if err != nil { return nil, err } defer rows.Close() var result []models.UserModelSetting for rows.Next() { var s models.UserModelSetting var prefTemp, prefMaxTokens interface{} err := rows.Scan( &s.ID, &s.UserID, &s.ModelID, &s.Hidden, &prefTemp, &prefMaxTokens, &s.SortOrder, &s.CreatedAt, &s.UpdatedAt, ) if err != nil { return nil, err } if f, ok := prefTemp.(float64); ok { s.PreferredTemperature = &f } if n, ok := prefMaxTokens.(int64); ok { v := int(n) s.PreferredMaxTokens = &v } result = append(result, s) } return result, rows.Err() } // GetHiddenModelIDs returns a map of model_id → true for all hidden models. func (s *UserModelSettingsStore) GetHiddenModelIDs(ctx context.Context, userID string) (map[string]bool, error) { rows, err := DB.QueryContext(ctx, "SELECT model_id FROM user_model_settings WHERE user_id = $1 AND hidden = true", userID) if err != nil { return nil, err } defer rows.Close() result := make(map[string]bool) for rows.Next() { var modelID string if err := rows.Scan(&modelID); err != nil { return nil, err } result[modelID] = true } return result, rows.Err() } // Set upserts a single user model setting. func (s *UserModelSettingsStore) Set(ctx context.Context, userID, modelID string, patch models.UserModelSettingPatch) error { b := NewUpdate("user_model_settings") if patch.Hidden != nil { b.Set("hidden", *patch.Hidden) } if patch.PreferredTemperature != nil { b.Set("preferred_temperature", models.NullFloat(patch.PreferredTemperature)) } if patch.PreferredMaxTokens != nil { b.Set("preferred_max_tokens", models.NullInt(patch.PreferredMaxTokens)) } if patch.SortOrder != nil { b.Set("sort_order", *patch.SortOrder) } if !b.HasSets() { return nil } // Use upsert: insert if not exists, update if exists // Build a custom upsert since the update builder doesn't handle INSERT ON CONFLICT _, err := DB.ExecContext(ctx, ` INSERT INTO user_model_settings (user_id, model_id, hidden, preferred_temperature, preferred_max_tokens, sort_order) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (user_id, model_id) DO UPDATE SET hidden = COALESCE($3, user_model_settings.hidden), preferred_temperature = COALESCE($4, user_model_settings.preferred_temperature), preferred_max_tokens = COALESCE($5, user_model_settings.preferred_max_tokens), sort_order = COALESCE($6, user_model_settings.sort_order)`, userID, modelID, patchBoolOrNil(patch.Hidden), patchFloat64OrNil(patch.PreferredTemperature), patchIntOrNil(patch.PreferredMaxTokens), patchIntOrNil(patch.SortOrder), ) return err } // BulkSetHidden sets the hidden state for multiple models at once. func (s *UserModelSettingsStore) BulkSetHidden(ctx context.Context, userID string, modelIDs []string, hidden bool) error { if len(modelIDs) == 0 { return nil } // Build parameterized IN clause placeholders := make([]string, len(modelIDs)) args := make([]interface{}, 0, len(modelIDs)+2) args = append(args, userID, hidden) for i, id := range modelIDs { placeholders[i] = fmt.Sprintf("$%d", i+3) args = append(args, id) } // Upsert each: some might not have rows yet for _, modelID := range modelIDs { _, err := DB.ExecContext(ctx, ` INSERT INTO user_model_settings (user_id, model_id, hidden) VALUES ($1, $2, $3) ON CONFLICT (user_id, model_id) DO UPDATE SET hidden = $3`, userID, modelID, hidden) if err != nil { return fmt.Errorf("set hidden for %s: %w", modelID, err) } } return nil } // ── Helpers ───────────────────────────────── func patchBoolOrNil(b *bool) interface{} { if b == nil { return nil } return *b } func patchFloat64OrNil(f *float64) interface{} { if f == nil { return nil } return *f } func patchIntOrNil(i *int) interface{} { if i == nil { return nil } return *i } // unused but keeping for reference - will be used in ListOptions-based queries var _ = strings.Join