package domain import ( "database/sql/driver" "encoding/json" "fmt" "strconv" "strings" "time" ) type Duration struct { time.Duration } func (d Duration) MarshalJSON() ([]byte, error) { return json.Marshal(d.Duration.String()) } func (d *Duration) UnmarshalJSON(b []byte) error { var s string if err := json.Unmarshal(b, &s); err != nil { return err } duration, err := time.ParseDuration(s) if err != nil { return err } d.Duration = duration return nil } func (d Duration) Value() (driver.Value, error) { // Store as a PostgreSQL-compatible interval string return d.Duration.String(), nil } func (d *Duration) Scan(value interface{}) error { if value == nil { d.Duration = 0 return nil } switch v := value.(type) { case int64: // Handle legacy nanosecond values that were incorrectly stored // If the value is extremely large (likely nanoseconds), convert it if v > 1000000000000 { // More than 16 minutes in nanoseconds, likely a nanosecond value d.Duration = time.Duration(v) } else { // Assume it's seconds for smaller values d.Duration = time.Duration(v) * time.Second } case string: duration, err := time.ParseDuration(v) if err != nil { return fmt.Errorf("cannot parse duration string: %s", v) } d.Duration = duration case []uint8: // Handle PostgreSQL interval format intervalStr := string(v) // Try parsing as Go duration first (for newer records) if duration, err := time.ParseDuration(intervalStr); err == nil { d.Duration = duration return nil } // Handle PostgreSQL interval formats like "00:00:30" or "8333333:20:00" if strings.Contains(intervalStr, ":") { parts := strings.Split(intervalStr, ":") if len(parts) >= 2 { var hours, minutes, seconds float64 var err error switch len(parts) { case 2: // MM:SS minutes, err = strconv.ParseFloat(parts[0], 64) if err != nil { return fmt.Errorf("cannot parse minutes from interval: %s", intervalStr) } seconds, err = strconv.ParseFloat(parts[1], 64) if err != nil { return fmt.Errorf("cannot parse seconds from interval: %s", intervalStr) } case 3: // HH:MM:SS hours, err = strconv.ParseFloat(parts[0], 64) if err != nil { return fmt.Errorf("cannot parse hours from interval: %s", intervalStr) } minutes, err = strconv.ParseFloat(parts[1], 64) if err != nil { return fmt.Errorf("cannot parse minutes from interval: %s", intervalStr) } seconds, err = strconv.ParseFloat(parts[2], 64) if err != nil { return fmt.Errorf("cannot parse seconds from interval: %s", intervalStr) } default: return fmt.Errorf("unsupported interval format: %s", intervalStr) } // Convert to duration totalSeconds := hours*3600 + minutes*60 + seconds d.Duration = time.Duration(totalSeconds * float64(time.Second)) return nil } } // Handle PostgreSQL interval format like "30 seconds", "1 minute", etc. if strings.Contains(intervalStr, " ") { // Try to parse common PostgreSQL interval formats intervalStr = strings.TrimSpace(intervalStr) // Replace PostgreSQL interval keywords with Go duration format intervalStr = strings.ReplaceAll(intervalStr, " seconds", "s") intervalStr = strings.ReplaceAll(intervalStr, " second", "s") intervalStr = strings.ReplaceAll(intervalStr, " minutes", "m") intervalStr = strings.ReplaceAll(intervalStr, " minute", "m") intervalStr = strings.ReplaceAll(intervalStr, " hours", "h") intervalStr = strings.ReplaceAll(intervalStr, " hour", "h") if duration, err := time.ParseDuration(intervalStr); err == nil { d.Duration = duration return nil } } return fmt.Errorf("cannot parse PostgreSQL interval format: %s", intervalStr) default: return fmt.Errorf("cannot scan %T into Duration", value) } return nil } func ParseDuration(s string) (Duration, error) { if s == "" { return Duration{}, fmt.Errorf("empty duration string") } s = strings.TrimSpace(s) duration, err := time.ParseDuration(s) if err != nil { return Duration{}, fmt.Errorf("failed to parse duration '%s': %v", s, err) } return Duration{Duration: duration}, nil } func (d Duration) String() string { return d.Duration.String() } func (d Duration) Seconds() float64 { return d.Duration.Seconds() } func (d Duration) Minutes() float64 { return d.Duration.Minutes() } func (d Duration) Hours() float64 { return d.Duration.Hours() }