Files
core/internal/idgen/generator.go

133 lines
3.6 KiB
Go

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"
LotModelMapping EntityType = "lot_model_mapping"
)
// 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",
LotModelMapping: "LM",
}
// 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
}