gastownhall/beads · error

legacy SQLite issue has empty ID

Error message

legacy SQLite issue has empty ID

What it means

checkRequiredScalars enforces that every legacy issue has a non-empty ID. This error means a row was read whose ID column is empty, which the current schema cannot represent — every issue requires a unique identifier. The migration aborts for that row.

Source

Thrown at internal/migration/legacysqlite/reader.go:553

// applyCanonicalTimestamps normalizes the required created_at/updated_at values
// to the canonical current-schema representation.
func (x legacyExtras) applyCanonicalTimestamps(issue *types.Issue) error {
	var err error
	if issue.CreatedAt, err = canonicalCurrentDatetime(issue.CreatedAt); err != nil {
		return fmt.Errorf("legacy SQLite issue %s created_at: %w", issue.ID, err)
	}
	if issue.UpdatedAt, err = canonicalCurrentDatetime(issue.UpdatedAt); err != nil {
		return fmt.Errorf("legacy SQLite issue %s updated_at: %w", issue.ID, err)
	}
	return nil
}

// checkRequiredScalars enforces the non-empty ID, non-tombstone status, and
// present created_at/updated_at invariants every legacy issue must satisfy.
func checkRequiredScalars(issue *types.Issue) error {
	if issue.ID == "" {
		return fmt.Errorf("legacy SQLite issue has empty ID")
	}
	if issue.Status == "tombstone" {
		return fmt.Errorf("legacy SQLite issue %s is a tombstone", issue.ID)
	}
	if issue.CreatedAt.IsZero() || issue.UpdatedAt.IsZero() {
		return fmt.Errorf("legacy SQLite issue %s has invalid created_at or updated_at", issue.ID)
	}
	return nil
}

// checkRemovedFields rejects legacy rows that populate columns the current
// schema no longer supports, then validates the tri-state boolean columns.
func (x legacyExtras) checkRemovedFields(issue *types.Issue) error {
	if nonempty(x.closedBy, x.deletedBy, x.deleteReason, x.originalType, x.hookBead, x.roleBead, x.agentState, x.lastActivity, x.roleType, x.rig) || x.deletedAt.Valid || x.crystallizes.Int64 != 0 || x.quality.Valid || (issue.SourceRepo != "" && issue.SourceRepo != ".") {
		return fmt.Errorf("legacy SQLite issue %s uses unsupported removed fields", issue.ID)
	}
	for _, b := range []struct {
		name string

View on GitHub (pinned to 71377f2769)

Solutions

  1. Find the bad row: sqlite3 legacy.db "SELECT rowid FROM issues WHERE id = '' OR id IS NULL"
  2. Delete the row if it is junk, or assign it a proper unique ID in the legacy DB's ID format (e.g. 'bd-<n>' or the legacy prefix convention)
  3. Check for other rows missing required fields in the same import batch
  4. Re-run the migration

Example fix

// before
UPDATE issues SET ... (row with empty id left in place)
// after
DELETE FROM issues WHERE id = '' OR id IS NULL;
-- or
UPDATE issues SET id = 'bd-1234' WHERE rowid = <rowid>;
Defensive patterns

Strategy: validation

Validate before calling

-- run before migration
SELECT rowid FROM issues WHERE id IS NULL OR trim(id) = '';

Type guard

def has_valid_id(row) -> bool:
    return isinstance(row.get('id'), str) and row['id'].strip() != ''

Prevention

When it happens

Trigger: The legacy issues table contains a row with an empty (or whitespace) id value — typically a manually inserted row, a bulk import that left ids blank, or corruption from a failed write.

Common situations: Hand-rolled scripts or ETL jobs inserting issues without generating IDs; databases restored from partial backups.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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