{"record":{"id":"0937a82e58e1c811","repo":"gastownhall/beads","slug":"invalid-identifier-q","errorCode":null,"errorMessage":"invalid identifier: %q","messagePattern":"invalid identifier: %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/ddl.go","lineNumber":20,"sourceCode":"\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n)\n\nvar validIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)\n\nconst maxIdentifierLength = 64\n\n// ValidateIdentifier checks whether name is safe to use, unquoted, as a\n// database or table identifier in this package's DDL statements.\nfunc ValidateIdentifier(name string) error {\n\tif len(name) > maxIdentifierLength {\n\t\treturn fmt.Errorf(\"identifier too long: %q (max %d chars)\", name, maxIdentifierLength)\n\t}\n\tif !validIdentifier.MatchString(name) {\n\t\treturn fmt.Errorf(\"invalid identifier: %q\", name)\n\t}\n\treturn nil\n}\n\ntype DDLSQLRepository interface {\n\tCreateDatabaseIfNotExists(ctx context.Context, database string) error\n\t// CreateDatabase issues a bare CREATE DATABASE (no IF NOT EXISTS) so the\n\t// server arbitrates creation atomically: success proves this call created\n\t// the database; an already-exists error (MySQL 1007) proves it did not.\n\t// The error is returned unmapped (wrapped with %w) so callers can\n\t// classify it against their driver.\n\tCreateDatabase(ctx context.Context, database string) error\n\tUseDatabase(ctx context.Context, database string) error\n}\n\nfunc NewDDLSQLRepository(runner Runner) DDLSQLRepository {\n\treturn &ddlSQLRepository{runner: runner}\n}","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/ddl.go#L2-L38","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Sanitize the name: replace invalid characters with underscores and ensure it starts with a letter or underscore.","Call db.ValidateIdentifier(name) before invoking DDL APIs to fail fast.","Fix the source of the bad name (env var, config, or derived slug).","Never attempt to pass user-controlled strings with quotes/backticks — rely on the library's validation."],"exampleFix":"// before\nerr := ddl.CreateDatabaseIfNotExists(ctx, \"my-project.dev\") // hyphen/dot rejected\n// after\nname := strings.ReplaceAll(strings.ReplaceAll(\"my-project.dev\", \"-\", \"_\"), \".\", \"_\")\nif err := db.ValidateIdentifier(name); err != nil { return err }\nerr := ddl.CreateDatabaseIfNotExists(ctx, name)","handlingStrategy":"validation","validationCode":"var identRe = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)\nfunc checkIdent(name string) error {\n    if !identRe.MatchString(name) {\n        return fmt.Errorf(\"name %q must match [a-zA-Z_][a-zA-Z0-9_]*\", name)\n    }\n    return db.ValidateIdentifier(name)\n}","typeGuard":"func isSafeIdentifier(name string) bool {\n    return len(name) > 0 && len(name) <= 64 &&\n        regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`).MatchString(name)\n}","tryCatchPattern":"if err := db.ValidateIdentifier(name); err != nil {\n    return fmt.Errorf(\"invalid database name %q: %w\", name, err)\n}\nerr := ddl.CreateDatabaseIfNotExists(ctx, name)","preventionTips":["Normalize names early: replace -, ., /, and spaces with underscores; prefix a letter if the name starts with a digit.","Reject empty names from env/config before calling DDL APIs.","Never build identifiers from untrusted input without validation — this check is your SQL-injection guard."],"tags":["validation","identifier","sql-injection"],"backgroundTag":"identifier-validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}