package store import ( "database/sql" "errors" "fmt" "github.com/google/uuid" ) // SetWorkloadEnv upserts a single env var for the workload. Uses // (workload_id, key) as the natural key — duplicate keys collapse onto // the same row instead of accumulating. func (s *Store) SetWorkloadEnv(env WorkloadEnv) (WorkloadEnv, error) { if env.WorkloadID == "" || env.Key == "" { return WorkloadEnv{}, fmt.Errorf("workload_env: workload_id and key are required") } now := Now() if env.ID == "" { env.ID = uuid.New().String() } env.UpdatedAt = now // Try INSERT first; on UNIQUE violation, fall through to UPDATE so the // row's ID + created_at survive. _, err := s.db.Exec( `INSERT INTO workload_env (id, workload_id, key, value, encrypted, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(workload_id, key) DO UPDATE SET value = excluded.value, encrypted = excluded.encrypted, updated_at = excluded.updated_at`, env.ID, env.WorkloadID, env.Key, env.Value, BoolToInt(env.Encrypted), now, now, ) if err != nil { return WorkloadEnv{}, fmt.Errorf("upsert workload env: %w", err) } // Re-read so the caller gets the canonical row (ID may differ when // the conflict path took over an older row). row, err := s.getWorkloadEnvByKey(env.WorkloadID, env.Key) if err != nil { return WorkloadEnv{}, err } return row, nil } // ListWorkloadEnv returns every env var for a workload, ordered by key. func (s *Store) ListWorkloadEnv(workloadID string) ([]WorkloadEnv, error) { rows, err := s.db.Query( `SELECT id, workload_id, key, value, encrypted, created_at, updated_at FROM workload_env WHERE workload_id = ? ORDER BY key`, workloadID, ) if err != nil { return nil, fmt.Errorf("query workload env: %w", err) } defer rows.Close() out := []WorkloadEnv{} for rows.Next() { env, err := scanWorkloadEnvRows(rows) if err != nil { return nil, err } out = append(out, env) } return out, rows.Err() } // DeleteWorkloadEnv removes one env var by ID. func (s *Store) DeleteWorkloadEnv(id string) error { result, err := s.db.Exec(`DELETE FROM workload_env WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete workload env: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("workload env %s: %w", id, ErrNotFound) } return nil } // getWorkloadEnvByKey is the upsert's re-read helper. func (s *Store) getWorkloadEnvByKey(workloadID, key string) (WorkloadEnv, error) { var env WorkloadEnv var enc int err := s.db.QueryRow( `SELECT id, workload_id, key, value, encrypted, created_at, updated_at FROM workload_env WHERE workload_id = ? AND key = ?`, workloadID, key, ).Scan(&env.ID, &env.WorkloadID, &env.Key, &env.Value, &enc, &env.CreatedAt, &env.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return WorkloadEnv{}, fmt.Errorf("workload env (%s,%s): %w", workloadID, key, ErrNotFound) } if err != nil { return WorkloadEnv{}, fmt.Errorf("query workload env: %w", err) } env.Encrypted = enc != 0 return env, nil } func scanWorkloadEnvRows(rows *sql.Rows) (WorkloadEnv, error) { var env WorkloadEnv var enc int if err := rows.Scan(&env.ID, &env.WorkloadID, &env.Key, &env.Value, &enc, &env.CreatedAt, &env.UpdatedAt); err != nil { return WorkloadEnv{}, fmt.Errorf("scan workload env: %w", err) } env.Encrypted = enc != 0 return env, nil }