package repository import ( "context" "fmt" "strconv" "time" "xprem/internal/database" "xprem/internal/database/postgres/pgdb" "xprem/internal/types" "github.com/jackc/pgx/v5/pgtype" ) type PostgresBranchRepository struct { engine *database.Engine } func NewPostgresBranchRepository(engine *database.Engine) *PostgresBranchRepository { return &PostgresBranchRepository{ engine: engine, } } func (s *PostgresBranchRepository) InsertBranch(ctx context.Context, appId string, branchName string) (int64, error) { insertedId, err := s.engine.Queries.InsertBranch(ctx, pgdb.InsertBranchParams{AppID: ToPgUUID(appId), Name: branchName}) if err != nil { if database.IsUniqueViolation(err) { return 0, &ErrResourceAlreadyExists{Resource: "branch", Identifier: branchName} } return 0, err } return insertedId, nil } func (s *PostgresBranchRepository) GetUpdateRefsByBranchName(ctx context.Context, appId string, branchName string) ([]types.UpdateRef, error) { rows, err := s.engine.Queries.GetUpdatesMetadataByBranchName(ctx, pgdb.GetUpdatesMetadataByBranchNameParams{ Name: branchName, AppID: ToPgUUID(appId), }) if err != nil { return nil, err } refs := make([]types.UpdateRef, len(rows)) for i, row := range rows { refs[i] = types.UpdateRef{ID: row.ID, RuntimeVersion: row.RuntimeVersion} } return refs, nil } func (s *PostgresBranchRepository) DeleteBranchByName(ctx context.Context, appId string, branchName string) error { pgAppID := ToPgUUID(appId) commandTag, err := s.engine.Queries.DeleteBranchByName(ctx, pgdb.DeleteBranchByNameParams{ Name: branchName, AppID: pgAppID, }) if err != nil { return err } if commandTag.RowsAffected() == 0 { // The DELETE refuses protected branches inside the statement, so a miss is // either that refusal or a missing branch; the follow-up read picks the right error. protected, protErr := s.engine.Queries.IsBranchProtected(ctx, pgdb.IsBranchProtectedParams{ AppID: pgAppID, Name: branchName, }) if protErr != nil { if database.IsNoRows(protErr) { return &ErrResourceNotFound{Resource: "branch", Identifier: fmt.Sprintf("name: %s, appId: %s", branchName, appId)} } return fmt.Errorf("failed to check branch protection after a refused delete: %w", protErr) } if protected { return &ErrBranchProtected{BranchName: branchName} } return &ErrResourceNotFound{Resource: "branch", Identifier: fmt.Sprintf("name: %s, appId: %s", branchName, appId)} } return nil } func (s *PostgresBranchRepository) GetSurfableBranches(ctx context.Context, appId string, runtimeVersion string, platform types.Platform) ([]types.SurfableBranch, error) { pgAppID := ToPgUUID(appId) rows, err := s.engine.Queries.GetSurfableBranches(ctx, pgdb.GetSurfableBranchesParams{ AppID: pgAppID, Version: runtimeVersion, Platform: string(platform), }) if err != nil { return nil, fmt.Errorf("failed to retrieve surfable branches from database: %w", err) } branches := make([]types.SurfableBranch, len(rows)) for i, row := range rows { branches[i] = types.SurfableBranch{ Name: row.BranchName, LastUpdateAt: row.LastUpdateAt.Time.UTC().Format(time.RFC3339), } } return branches, nil } func (s *PostgresBranchRepository) GetBranches(ctx context.Context, appId string) ([]types.BranchMapping, error) { pgAppID := ToPgUUID(appId) appBranches, err := s.engine.Queries.GetBranchesByAppID(ctx, pgAppID) if err != nil { return nil, fmt.Errorf("failed to retrieve branches from database: %w", err) } branches := make([]types.BranchMapping, len(appBranches)) for i, branch := range appBranches { var createdAtStr *string if branch.CreatedAt.Valid { timeStr := branch.CreatedAt.Time.Format(time.RFC3339) createdAtStr = &timeStr } branchIdStr := strconv.FormatInt(branch.ID, 10) branches[i] = types.BranchMapping{ BranchName: branch.Name, BranchId: &branchIdStr, ReleaseChannel: branch.ChannelName, CreatedAt: createdAtStr, Protected: branch.Protected, CurrentUpdate: branchUpdateState( branch.CurrentRuntimeVersion, branch.CurrentCommitHash, branch.CurrentUpdateCreatedAt, branch.CurrentRolloutPercentage, ), } } return branches, nil } func branchUpdateState(runtimeVersion *string, commitHash *string, createdAt pgtype.Timestamptz, rolloutPercentage *int32) *types.BranchUpdateState { if runtimeVersion == nil || commitHash == nil || !createdAt.Valid { return nil } var percentage *int if rolloutPercentage != nil { value := int(*rolloutPercentage) percentage = &value } return &types.BranchUpdateState{ RuntimeVersion: *runtimeVersion, CommitHash: *commitHash, CreatedAt: createdAt.Time.Format(time.RFC3339), RolloutPercentage: percentage, } } func (s *PostgresBranchRepository) UpsertBranchAndRuntimeVersion(ctx context.Context, appId string, branchName string, runtimeVersion string) error { _, err := s.InsertBranch(ctx, appId, branchName) if err != nil { if _, ok := err.(*ErrResourceAlreadyExists); ok { err = nil } else { return fmt.Errorf("failed to upsert branch: %w", err) } } _, err = s.CreateRuntimeVersion(ctx, appId, runtimeVersion) if err != nil { if _, ok := err.(*ErrResourceAlreadyExists); ok { err = nil } else { return fmt.Errorf("failed to upsert runtime version: %w", err) } } return err } func (s *PostgresBranchRepository) GetRuntimeVersionsWithUpdateStats(ctx context.Context, appId string, branchName string) ([]types.RuntimeVersionWithStats, error) { pgAppID := ToPgUUID(appId) rows, err := s.engine.Queries.GetRuntimeVersionsWithUpdateCount(ctx, pgdb.GetRuntimeVersionsWithUpdateCountParams{ AppID: pgAppID, Name: branchName, }) if err != nil { return nil, fmt.Errorf("failed to retrieve runtime versions with update stats from database: %w", err) } runtimeVersions := make([]types.RuntimeVersionWithStats, len(rows)) for i, row := range rows { var lastUpdatedAtStr string if row.UpdatedAt.Valid { lastUpdatedAtStr = row.UpdatedAt.Time.Format(time.RFC3339) } else { lastUpdatedAtStr = "" } var createdAtStr string if row.CreatedAt.Valid { createdAtStr = row.CreatedAt.Time.Format(time.RFC3339) } else { createdAtStr = "" } runtimeVersion := types.RuntimeVersionWithStats{ RuntimeVersion: row.Version, LastUpdatedAt: lastUpdatedAtStr, CreatedAt: createdAtStr, NumberOfUpdates: int(row.UpdateCount), } // sqlc cannot type an aggregate subquery, so RolloutPercentage comes // back as interface{}: pgx yields int32 for a value, nil for SQL NULL. if pct, ok := row.RolloutPercentage.(int32); ok { percentage := int(pct) runtimeVersion.ActiveRollout = true runtimeVersion.RolloutPercentage = &percentage } runtimeVersions[i] = runtimeVersion } return runtimeVersions, nil } func (s *PostgresBranchRepository) UpdateChannelBranchMapping(ctx context.Context, appId string, channelId string, branchId string) error { channelIdInt, err := strconv.ParseInt(channelId, 10, 64) if err != nil { return fmt.Errorf("invalid channel ID: %w", err) } branchIdInt, err := strconv.ParseInt(branchId, 10, 64) if err != nil { return fmt.Errorf("invalid branch ID: %w", err) } branchIdIntPtr := &branchIdInt pgAppID := ToPgUUID(appId) commandTag, err := s.engine.Queries.UpdateChannelBranchMapping(ctx, pgdb.UpdateChannelBranchMappingParams{ AppID: pgAppID, BranchID: branchIdIntPtr, ID: channelIdInt, }) if err != nil { if database.IsForeignKeyViolation(err) { return &ErrResourceNotFound{Resource: "branch", Identifier: fmt.Sprintf("%s (appId: %s)", branchId, appId)} } return fmt.Errorf("failed to update channel-branch mapping in database: %w", err) } if commandTag.RowsAffected() == 0 { return &ErrResourceNotFound{Resource: "channel", Identifier: fmt.Sprintf("%s (appId: %s)", channelId, appId)} } return nil } func (s *PostgresBranchRepository) CreateRuntimeVersion(ctx context.Context, appId string, version string) (int64, error) { pgAppID := ToPgUUID(appId) _, err := s.engine.Queries.InsertRuntimeVersion(ctx, pgdb.InsertRuntimeVersionParams{ AppID: pgAppID, Version: version, }) if err != nil { if database.IsUniqueViolation(err) { return 0, &ErrResourceAlreadyExists{Resource: "runtime version", Identifier: fmt.Sprintf("%s (appId: %s)", version, appId)} } return 0, fmt.Errorf("failed to create runtime version in database: %w", err) } return 0, nil } func (s *PostgresBranchRepository) GetBranchByName(ctx context.Context, appId string, branchName string) (int64, error) { pgAppID := ToPgUUID(appId) return s.engine.Queries.GetBranchByName(ctx, pgdb.GetBranchByNameParams{ Name: branchName, AppID: pgAppID, }) }