gastownhall/beads · error

invalid identifier: %q

Error message

invalid identifier: %q

What it means

ValidateIdentifier enforces the pattern ^[a-zA-Z_][a-zA-Z0-9_]*$ so identifiers are safe to embed unquoted in DDL, preventing syntax errors and SQL injection through database/table names. This error means the name contains characters outside letters, digits, and underscores, or starts with a digit. It is returned by QuoteIdentifier and wrapped by DDL methods.

Source

Thrown at internal/storage/domain/db/ddl.go:20

import (
	"context"
	"fmt"
	"regexp"
)

var validIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)

const maxIdentifierLength = 64

// ValidateIdentifier checks whether name is safe to use, unquoted, as a
// database or table identifier in this package's DDL statements.
func ValidateIdentifier(name string) error {
	if len(name) > maxIdentifierLength {
		return fmt.Errorf("identifier too long: %q (max %d chars)", name, maxIdentifierLength)
	}
	if !validIdentifier.MatchString(name) {
		return fmt.Errorf("invalid identifier: %q", name)
	}
	return nil
}

type DDLSQLRepository interface {
	CreateDatabaseIfNotExists(ctx context.Context, database string) error
	// CreateDatabase issues a bare CREATE DATABASE (no IF NOT EXISTS) so the
	// server arbitrates creation atomically: success proves this call created
	// the database; an already-exists error (MySQL 1007) proves it did not.
	// The error is returned unmapped (wrapped with %w) so callers can
	// classify it against their driver.
	CreateDatabase(ctx context.Context, database string) error
	UseDatabase(ctx context.Context, database string) error
}

func NewDDLSQLRepository(runner Runner) DDLSQLRepository {
	return &ddlSQLRepository{runner: runner}
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Sanitize the name: replace invalid characters with underscores and ensure it starts with a letter or underscore.
  2. Call db.ValidateIdentifier(name) before invoking DDL APIs to fail fast.
  3. Fix the source of the bad name (env var, config, or derived slug).
  4. Never attempt to pass user-controlled strings with quotes/backticks — rely on the library's validation.

Example fix

// before
err := ddl.CreateDatabaseIfNotExists(ctx, "my-project.dev") // hyphen/dot rejected
// after
name := strings.ReplaceAll(strings.ReplaceAll("my-project.dev", "-", "_"), ".", "_")
if err := db.ValidateIdentifier(name); err != nil { return err }
err := ddl.CreateDatabaseIfNotExists(ctx, name)
Defensive patterns

Strategy: validation

Validate before calling

var identRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
func checkIdent(name string) error {
    if !identRe.MatchString(name) {
        return fmt.Errorf("name %q must match [a-zA-Z_][a-zA-Z0-9_]*", name)
    }
    return db.ValidateIdentifier(name)
}

Type guard

func isSafeIdentifier(name string) bool {
    return len(name) > 0 && len(name) <= 64 &&
        regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`).MatchString(name)
}

Try / catch

if err := db.ValidateIdentifier(name); err != nil {
    return fmt.Errorf("invalid database name %q: %w", name, err)
}
err := ddl.CreateDatabaseIfNotExists(ctx, name)

Prevention

When it happens

Trigger: Calling CreateDatabaseIfNotExists/CreateDatabase/UseDatabase (or QuoteIdentifier) with a name containing hyphens, dots, spaces, slashes, or other non-[A-Za-z0-9_] characters, or a name starting with a digit, or an empty name.

Common situations: Using filesystem-style or DNS-style names (my-project.dev) as database names; empty BD_DB env var; names built from paths or URLs; identifiers copied from other systems that allow quoting.

Related errors


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