package idgen import ( "context" "database/sql" "fmt" ) // EntityType represents the type of entity for ID generation type EntityType string const ( Customer EntityType = "customer" Project EntityType = "project" Location EntityType = "location" Lot EntityType = "lot" Asset EntityType = "machine" // Renamed from asset Component EntityType = "part" // Renamed from component Installation EntityType = "installation" LogBundle EntityType = "log_bundle" Observation EntityType = "observation" TimelineEvent EntityType = "timeline_event" Ticket EntityType = "ticket" TicketLink EntityType = "ticket_link" FailureEvent EntityType = "failure_event" ) // Prefix mapping for each entity type var prefixMap = map[EntityType]string{ Customer: "CR", Project: "PJ", Location: "LN", Lot: "LT", Asset: "ME", // Machine Component: "PT", // Part Installation: "IN", LogBundle: "LB", Observation: "OB", TimelineEvent: "TE", Ticket: "TT", TicketLink: "TL", FailureEvent: "FE", } // Generator handles unique ID generation with prefixes type Generator struct { db *sql.DB } // NewGenerator creates a new ID generator func NewGenerator(db *sql.DB) *Generator { return &Generator{db: db} } // Generate creates a new unique ID for the given entity type // Format: PREFIX-NNNNNNN (e.g., CR-0000001) // This uses row-level locking to ensure thread-safe sequence generation func (g *Generator) Generate(ctx context.Context, entityType EntityType) (string, error) { prefix, ok := prefixMap[entityType] if !ok { return "", fmt.Errorf("unknown entity type: %s", entityType) } // Start transaction for atomic sequence increment tx, err := g.db.BeginTx(ctx, nil) if err != nil { return "", fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() // Lock the row and get current sequence value var nextValue int64 err = tx.QueryRowContext(ctx, `SELECT next_value FROM id_sequences WHERE entity_type = ? FOR UPDATE`, string(entityType), ).Scan(&nextValue) if err != nil { if err == sql.ErrNoRows { return "", fmt.Errorf("sequence not found for entity type: %s", entityType) } return "", fmt.Errorf("failed to get sequence: %w", err) } // Increment the sequence _, err = tx.ExecContext(ctx, `UPDATE id_sequences SET next_value = next_value + 1 WHERE entity_type = ?`, string(entityType), ) if err != nil { return "", fmt.Errorf("failed to update sequence: %w", err) } // Commit transaction if err := tx.Commit(); err != nil { return "", fmt.Errorf("failed to commit transaction: %w", err) } // Format the ID with prefix and zero-padded number return FormatID(prefix, nextValue), nil } // FormatID formats a numeric ID with the given prefix // Example: FormatID("CR", 1) returns "CR-0000001" func FormatID(prefix string, number int64) string { return fmt.Sprintf("%s-%07d", prefix, number) } // ParseID extracts the numeric part from a formatted ID // Example: ParseID("CR-0000001") returns 1, nil func ParseID(id string) (int64, error) { var number int64 var prefix string // Try to parse the formatted ID _, err := fmt.Sscanf(id, "%2s-%07d", &prefix, &number) if err != nil { return 0, fmt.Errorf("invalid ID format: %s", id) } return number, nil } // GetPrefix returns the prefix for a given entity type func GetPrefix(entityType EntityType) (string, error) { prefix, ok := prefixMap[entityType] if !ok { return "", fmt.Errorf("unknown entity type: %s", entityType) } return prefix, nil }