gastownhall/beads · error

invalid ref: %w

Error message

invalid ref: %w

What it means

ReadMigrationContentHashes wraps any validation failure from validateMigrationRef in "invalid ref: %w". The underlying cause (empty, too long, or bad characters) is always wrapped, so use errors.Is/Unwrap to see the specific reason. It is the entry guard before interpolating the ref into AS OF queries.

Source

Thrown at internal/storage/schema/migration_content_hashes.go:50

// ReadMigrationContentHashes reads version -> content_hash from schema_migrations,
// either at HEAD (ref == "") or AS OF ref (e.g. "remotes/origin/main"). NULL/empty
// hashes are dropped. It returns an error when the table, column, or ref is
// unavailable; the caller classifies it with RemoteRefUnavailableErr /
// MissingMigrationObjectErr.
//
// Dolt requires a literal ref in AS OF: bind parameters (including inside CONCAT)
// fail server-side with `unbound variable "v1" in query`, so the validated ref is
// interpolated into the SQL text (bd-6dnrw.27).
func ReadMigrationContentHashes(ctx context.Context, db DBConn, ref string) (map[int]string, error) {
	var (
		rows *sql.Rows
		err  error
	)
	if ref == "" {
		rows, err = db.QueryContext(ctx, "SELECT version, content_hash FROM schema_migrations")
	} else {
		if verr := validateMigrationRef(ref); verr != nil {
			return nil, fmt.Errorf("invalid ref: %w", verr)
		}
		// A cached ref may legitimately predate schema_migrations or its
		// content_hash column. Probe the historical shape before selecting from
		// it: letting the SELECT fail emits a Dolt server warning for every
		// read-only doctor run.
		//nolint:gosec // G201: ref is validated above — AS OF requires a literal, not a bind param
		hasTable, queryErr := queryHasRows(ctx, db,
			fmt.Sprintf("SHOW TABLES AS OF '%s' LIKE 'schema_migrations'", ref))
		if queryErr != nil {
			return nil, queryErr
		}
		if !hasTable {
			return nil, fmt.Errorf("table not found: schema_migrations at %q", ref)
		}
		//nolint:gosec // G201: ref is validated above — AS OF requires a literal, not a bind param
		hasContentHash, queryErr := queryHasRows(ctx, db,
			fmt.Sprintf("SHOW COLUMNS FROM schema_migrations AS OF '%s' LIKE 'content_hash'", ref))
		if queryErr != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix the ref to be a non-empty, <=128-char, allowlist-conforming string like "remotes/origin/main".
  2. Check the wrapped cause with errors.Is to know which rule failed.
  3. Pass "" only when HEAD is intended via the ref=="" code path.
  4. Sanitize ref inputs at the boundary (CLI/config) before calling.

Example fix

// before
hashes, err := schema.ReadMigrationContentHashes(ctx, db, raw)
// after
ref := strings.TrimSpace(raw)
if ref != "" && (len(ref) > 128 || !refRe.MatchString(ref)) {
    return fmt.Errorf("bad ref %q", ref)
}
hashes, err := schema.ReadMigrationContentHashes(ctx, db, ref)
Defensive patterns

Strategy: validation

Validate before calling

func migrationRefOK(ref string) bool {
    return ref == "" || (len(ref) <= 128 &&
        regexp.MustCompile(`^[a-zA-Z0-9_./-]+$`).MatchString(ref))
}

Type guard

func validRef(ref string) bool {
    return ref == "" || (len(ref) <= 128 &&
        regexp.MustCompile(`^[a-zA-Z0-9_./-]+$`).MatchString(ref))
}

Try / catch

if err != nil {
    var unwrapped = errors.Unwrap(err)
    // inspect wrapped cause: empty / too long / invalid format
}

Prevention

When it happens

Trigger: Calling ReadMigrationContentHashes (directly or via remoteMaxAtRef / routeSmartGate) with a ref that fails validation: empty, >128 chars, or containing characters outside [a-zA-Z0-9_./-].

Common situations: Same as the underlying validators: blank config values, pasted URLs, user-supplied refs with special characters; surfaced through doctor's read-only comparison of remote state.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/cad9fda9fc2dc43f. Report an issue: GitHub.