diff --git a/internal/state/aliases.go b/internal/state/aliases.go index e463c73..11d9e51 100644 --- a/internal/state/aliases.go +++ b/internal/state/aliases.go @@ -24,20 +24,17 @@ const aliasRetireSQL = "UPDATE aliases SET status = 'retired', retired_at = ? WH const aliasReactivateSQL = "UPDATE aliases SET status = 'active', retired_at = NULL, applied_at = ? WHERE repository_id = ? AND alias_path = ?" const aliasByPairPathSQL = ` -SELECT a.repository_id, a.alias_path, a.canonical_target_path, a.group_name, a.layer, - a.status, a.applied_at, a.retired_at +SELECT ` + aliasColumns + ` FROM aliases a JOIN repositories r ON r.id = a.repository_id WHERE r.root_path = ? AND r.home_path = ? AND a.alias_path = ?` const allAliasBaselinesSQL = ` -SELECT a.repository_id, a.alias_path, a.canonical_target_path, a.group_name, a.layer, - a.status, a.applied_at, a.retired_at +SELECT ` + aliasColumns + ` FROM aliases a JOIN repositories r ON r.id = a.repository_id WHERE r.root_path = ? AND r.home_path = ? ORDER BY a.alias_path` const activeAliasBaselinesSQL = ` -SELECT a.repository_id, a.alias_path, a.canonical_target_path, a.group_name, a.layer, - a.status, a.applied_at, a.retired_at +SELECT ` + aliasColumns + ` FROM aliases a JOIN repositories r ON r.id = a.repository_id WHERE r.root_path = ? AND r.home_path = ? AND a.status = 'active' ORDER BY a.alias_path` diff --git a/internal/state/aliases_read.go b/internal/state/aliases_read.go index 03a6c7d..fd60fc6 100644 --- a/internal/state/aliases_read.go +++ b/internal/state/aliases_read.go @@ -59,29 +59,26 @@ func (store *Store) setAliasStatus(key aliasBaselineKey, statement string) (Alia return AliasBaseline{}, fmt.Errorf("state: alias path %q is not a slash-relative path", key.alias) } now := formatTimestamp(store.now()) - transaction, err := store.database.conn.Begin() - if err != nil { - return AliasBaseline{}, err - } - if err := execIn(transaction, statement, now, repository.ID, key.alias); err != nil { - return AliasBaseline{}, err - } - row, err := scanAndCommitAlias(transaction, key) - if err != nil { - return AliasBaseline{}, err - } - return row, nil + return runStateTransaction(store.database.conn, + func(transaction *sql.Tx) error { return execIn(transaction, statement, now, repository.ID, key.alias) }, + func(transaction *sql.Tx) (AliasBaseline, error) { return readAliasBaselineAt(transaction, key) }) +} + +// readAliasBaselineAt scans one alias row of the pair, translating a missing +// row into errMissingAliasBaseline so callers get the path-specific error. +func readAliasBaselineAt(transaction *sql.Tx, key aliasBaselineKey) (AliasBaseline, error) { + baseline, err := scanAliasBaseline(transaction.QueryRow(aliasByPairPathSQL, key.root, key.home, key.alias)) + if errors.Is(err, sql.ErrNoRows) { + return AliasBaseline{}, errMissingAliasBaseline(key.alias) + } + return baseline, err } // scanAndCommitAlias reads the row back through the transaction and commits, // rolling back when the read fails so no open transaction leaks. func scanAndCommitAlias(transaction *sql.Tx, key aliasBaselineKey) (AliasBaseline, error) { return commitStateRead(transaction, func(transaction *sql.Tx) (AliasBaseline, error) { - baseline, err := scanAliasBaseline(transaction.QueryRow(aliasByPairPathSQL, key.root, key.home, key.alias)) - if errors.Is(err, sql.ErrNoRows) { - return AliasBaseline{}, errMissingAliasBaseline(key.alias) - } - return baseline, err + return readAliasBaselineAt(transaction, key) }) } @@ -109,23 +106,7 @@ func (store *Store) readAliasBaselines(statement, root, home string) ([]AliasBas } func (store *Store) readAliasGroups(statement, root, home string) ([]string, error) { - rows, err := store.database.conn.Query(statement, root, home) - if err != nil { - return nil, fmt.Errorf("state: list alias groups: %w", err) - } - defer rows.Close() - var groups []string - for rows.Next() { - var group string - if err := rows.Scan(&group); err != nil { - return nil, err - } - groups = append(groups, group) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("state: list alias groups: %w", err) - } - return groups, nil + return store.queryStrings(statement, "alias groups", root, home) } func errMissingAliasBaseline(aliasPath string) error { diff --git a/internal/state/files.go b/internal/state/files.go index 09394b4..6a8d7b6 100644 --- a/internal/state/files.go +++ b/internal/state/files.go @@ -30,23 +30,17 @@ const fileRetireSQL = "UPDATE files SET status = 'retired', retired_at = ? WHERE const fileReactivateSQL = "UPDATE files SET status = 'active', retired_at = NULL, applied_at = ? WHERE repository_id = ? AND target_path = ?" const fileByPairTargetSQL = ` -SELECT f.repository_id, f.target_path, f.group_name, f.source_path, f.source_kind, - f.layer, f.baseline_content_hash, f.baseline_source_hash, f.executable_bits, - f.status, f.applied_at, f.retired_at +SELECT ` + fileColumns + ` FROM files f JOIN repositories r ON r.id = f.repository_id WHERE r.root_path = ? AND r.home_path = ? AND f.target_path = ?` const allFileBaselinesSQL = ` -SELECT f.repository_id, f.target_path, f.group_name, f.source_path, f.source_kind, - f.layer, f.baseline_content_hash, f.baseline_source_hash, f.executable_bits, - f.status, f.applied_at, f.retired_at +SELECT ` + fileColumns + ` FROM files f JOIN repositories r ON r.id = f.repository_id WHERE r.root_path = ? AND r.home_path = ? ORDER BY f.target_path` const activeFileBaselinesSQL = ` -SELECT f.repository_id, f.target_path, f.group_name, f.source_path, f.source_kind, - f.layer, f.baseline_content_hash, f.baseline_source_hash, f.executable_bits, - f.status, f.applied_at, f.retired_at +SELECT ` + fileColumns + ` FROM files f JOIN repositories r ON r.id = f.repository_id WHERE r.root_path = ? AND r.home_path = ? AND f.status = 'active' ORDER BY f.target_path` diff --git a/internal/state/files_read.go b/internal/state/files_read.go index 7280962..ca63c9d 100644 --- a/internal/state/files_read.go +++ b/internal/state/files_read.go @@ -154,21 +154,28 @@ func (store *Store) checkRepresentationCorruption(root, home string) error { } func (store *Store) readFileGroups(statement, root, home string) ([]string, error) { - rows, err := store.database.conn.Query(statement, root, home) + return store.queryStrings(statement, "file groups", root, home) +} + +// queryStrings runs statement with args and returns the single string column +// from every row, wrapping query and iteration errors with label so each caller +// names its own read in the error. +func (store *Store) queryStrings(statement, label string, args ...any) ([]string, error) { + rows, err := store.database.conn.Query(statement, args...) if err != nil { - return nil, fmt.Errorf("state: list file groups: %w", err) + return nil, fmt.Errorf("state: list %s: %w", label, err) } defer rows.Close() - var groups []string + var values []string for rows.Next() { - var group string - if err := rows.Scan(&group); err != nil { + var value string + if err := rows.Scan(&value); err != nil { return nil, err } - groups = append(groups, group) + values = append(values, value) } if err := rows.Err(); err != nil { - return nil, fmt.Errorf("state: list file groups: %w", err) + return nil, fmt.Errorf("state: list %s: %w", label, err) } - return groups, nil + return values, nil } diff --git a/internal/state/recovery.go b/internal/state/recovery.go index 504f933..566605a 100644 --- a/internal/state/recovery.go +++ b/internal/state/recovery.go @@ -88,8 +88,3 @@ func errKeyRecoveryBlocked(path string, keyErr, idErr error) error { "state: hash key %q cannot be used; restore the matching hash.key or reset state (key: %v, identifier: %v)", path, keyErr, idErr) } - -// errKeyMissing reports that no usable key exists, helping operators recover. -func errKeyMissing(path string) error { - return fmt.Errorf("state: hash key %q is missing; restore it or reset state", path) -} diff --git a/internal/state/types.go b/internal/state/types.go index beaf8d5..1c66644 100644 --- a/internal/state/types.go +++ b/internal/state/types.go @@ -77,6 +77,13 @@ func (l AliasLayer) Valid() bool { return false } +// InactiveOn reports whether layer targets a platform other than platform. The +// "all" layer applies on every runtime so it is never inactive; a named platform +// layer is inactive when it does not equal the runtime platform. +func (l AliasLayer) InactiveOn(platform string) bool { + return l != LayerAll && string(l) != platform +} + // Repository is one registered (root, home) pair tracked by state. RootPath and // HomePath are canonical absolute identity anchors; all other paths are // relative.