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 stringView on GitHub (pinned to 71377f2769)
Solutions
- Find the bad row: sqlite3 legacy.db "SELECT rowid FROM issues WHERE id = '' OR id IS NULL"
- 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)
- Check for other rows missing required fields in the same import batch
- 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
- Enforce NOT NULL + non-empty id constraints in any custom writer
- Never bulk-insert issues without generating IDs via the bd CLI
- Check row counts against expectations before migrating
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
- issue %s waiters: %w
- legacy SQLite issue %s has invalid created_at or updated_at
- orphan label for %s
- dependency created_by is empty for %s -> %s
- sealed legacy SQLite database does not match source fingerpr
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/f769e1c37cbdc062.
Report an issue: GitHub.