diff --git a/README.md b/README.md index a672bd6..1d6c7d8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Potentially needs `export GOPRIVATE="gogs.mikescher.com"` | confext | Mike | Parses environment configuration into structs | | cmdext | Mike | Runner for external commands/processes | | | | | -| sq | Mike | Utility functions for sql based databases | +| sq | Mike | Utility functions for sql based databases (primarily sqlite) | | tst | Mike | Utility functions for unit tests | | | | | | rfctime | Mike | Classes for time seriallization, with different marshallign method for mongo and json | diff --git a/goextVersion.go b/goextVersion.go index a8c9c9c..4a4d2b0 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.398" +const GoextVersion = "0.0.399" -const GoextVersionTimestamp = "2024-03-09T13:36:06+0100" +const GoextVersionTimestamp = "2024-03-09T14:16:35+0100" diff --git a/sq/converter.go b/sq/converter.go index 1bab667..a85c2a5 100644 --- a/sq/converter.go +++ b/sq/converter.go @@ -1,17 +1,10 @@ package sq import ( - "encoding/json" "errors" "fmt" - "gogs.mikescher.com/BlackForestBytes/goext/exerr" "gogs.mikescher.com/BlackForestBytes/goext/langext" - "gogs.mikescher.com/BlackForestBytes/goext/rfctime" - "gogs.mikescher.com/BlackForestBytes/goext/timeext" "reflect" - "strconv" - "strings" - "time" ) type DBTypeConverter interface { @@ -21,169 +14,16 @@ type DBTypeConverter interface { DBToModel(v any) (any, error) } -var ConverterBoolToBit = NewDBTypeConverter[bool, int64](func(v bool) (int64, error) { - return langext.Conditional(v, int64(1), int64(0)), nil -}, func(v int64) (bool, error) { - if v == 0 { - return false, nil - } - if v == 1 { - return true, nil - } - return false, errors.New(fmt.Sprintf("invalid valud for boolean: '%d'", v)) -}) +type DBDataConstraint interface { + string | langext.NumberConstraint | []byte +} -var ConverterTimeToUnixMillis = NewDBTypeConverter[time.Time, int64](func(v time.Time) (int64, error) { - return v.UnixMilli(), nil -}, func(v int64) (time.Time, error) { - return time.UnixMilli(v), nil -}) +type DatabaseConvertible[TModelData any, TDBData DBDataConstraint] interface { + MarshalToDB(v TModelData) (TDBData, error) + UnmarshalToModel(v TDBData) (TModelData, error) +} -var ConverterRFCUnixMilliTimeToUnixMillis = NewDBTypeConverter[rfctime.UnixMilliTime, int64](func(v rfctime.UnixMilliTime) (int64, error) { - return v.UnixMilli(), nil -}, func(v int64) (rfctime.UnixMilliTime, error) { - return rfctime.NewUnixMilli(time.UnixMilli(v)), nil -}) - -var ConverterRFCUnixNanoTimeToUnixNanos = NewDBTypeConverter[rfctime.UnixNanoTime, int64](func(v rfctime.UnixNanoTime) (int64, error) { - return v.UnixNano(), nil -}, func(v int64) (rfctime.UnixNanoTime, error) { - return rfctime.NewUnixNano(time.Unix(0, v)), nil -}) - -var ConverterRFCUnixTimeToUnixSeconds = NewDBTypeConverter[rfctime.UnixTime, int64](func(v rfctime.UnixTime) (int64, error) { - return v.Unix(), nil -}, func(v int64) (rfctime.UnixTime, error) { - return rfctime.NewUnix(time.Unix(v, 0)), nil -}) - -// ConverterRFC339TimeToString -// Does not really use RFC339 - but sqlite does not understand timezones and the `T` delimiter -var ConverterRFC339TimeToString = NewDBTypeConverter[rfctime.RFC3339Time, string](func(v rfctime.RFC3339Time) (string, error) { - return v.Time().In(time.UTC).Format("2006-01-02 15:04:05"), nil -}, func(v string) (rfctime.RFC3339Time, error) { - t, err := time.Parse("2006-01-02 15:04:05", v) - if err != nil { - return rfctime.RFC3339Time{}, err - } - return rfctime.NewRFC3339(t), nil -}) - -// ConverterRFC339NanoTimeToString -// Does not really use RFC339 - but sqlite does not understand timezones and the `T` delimiter -var ConverterRFC339NanoTimeToString = NewDBTypeConverter[rfctime.RFC3339NanoTime, string](func(v rfctime.RFC3339NanoTime) (string, error) { - return v.Time().In(time.UTC).Format("2006-01-02 15:04:05.999999999"), nil -}, func(v string) (rfctime.RFC3339NanoTime, error) { - t, err := time.ParseInLocation("2006-01-02 15:04:05.999999999", v, time.UTC) - if err != nil { - return rfctime.RFC3339NanoTime{}, err - } - return rfctime.NewRFC3339Nano(t), nil -}) - -var ConverterRFCDateToString = NewDBTypeConverter[rfctime.Date, string](func(v rfctime.Date) (string, error) { - return fmt.Sprintf("%04d-%02d-%02d", v.Year, v.Month, v.Day), nil -}, func(v string) (rfctime.Date, error) { - split := strings.Split(v, "-") - if len(split) != 3 { - return rfctime.Date{}, errors.New("invalid date format: " + v) - } - year, err := strconv.ParseInt(split[0], 10, 32) - if err != nil { - return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) - } - month, err := strconv.ParseInt(split[0], 10, 32) - if err != nil { - return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) - } - day, err := strconv.ParseInt(split[0], 10, 32) - if err != nil { - return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) - } - - return rfctime.Date{Year: int(year), Month: int(month), Day: int(day)}, nil -}) - -var ConverterRFCTimeToString = NewDBTypeConverter[rfctime.Time, string](func(v rfctime.Time) (string, error) { - return v.SerializeShort(), nil -}, func(v string) (rfctime.Time, error) { - res := rfctime.Time{} - err := res.Deserialize(v) - if err != nil { - return rfctime.Time{}, err - } - return res, nil -}) - -var ConverterRFCSecondsF64ToString = NewDBTypeConverter[rfctime.SecondsF64, float64](func(v rfctime.SecondsF64) (float64, error) { - return v.Seconds(), nil -}, func(v float64) (rfctime.SecondsF64, error) { - return rfctime.NewSecondsF64(timeext.FromSeconds(v)), nil -}) - -var ConverterJsonObjToString = NewDBTypeConverter[JsonObj, string](func(v JsonObj) (string, error) { - mrsh, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(mrsh), nil -}, func(v string) (JsonObj, error) { - var mrsh JsonObj - if err := json.Unmarshal([]byte(v), &mrsh); err != nil { - return JsonObj{}, err - } - return mrsh, nil -}) - -var ConverterJsonArrToString = NewDBTypeConverter[JsonArr, string](func(v JsonArr) (string, error) { - mrsh, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(mrsh), nil -}, func(v string) (JsonArr, error) { - var mrsh JsonArr - if err := json.Unmarshal([]byte(v), &mrsh); err != nil { - return JsonArr{}, err - } - return mrsh, nil -}) - -var ConverterExErrCategoryToString = NewDBTypeConverter[exerr.ErrorCategory, string](func(v exerr.ErrorCategory) (string, error) { - return v.Category, nil -}, func(v string) (exerr.ErrorCategory, error) { - for _, cat := range exerr.AllCategories { - if cat.Category == v { - return cat, nil - } - } - return exerr.CatUser, errors.New("failed to convert '" + v + "' to exerr.ErrorCategory") -}) - -var ConverterExErrSeverityToString = NewDBTypeConverter[exerr.ErrorSeverity, string](func(v exerr.ErrorSeverity) (string, error) { - return v.Severity, nil -}, func(v string) (exerr.ErrorSeverity, error) { - for _, sev := range exerr.AllSeverities { - if sev.Severity == v { - return sev, nil - } - } - return exerr.SevErr, errors.New("failed to convert '" + v + "' to exerr.ErrorSeverity") -}) - -var ConverterExErrTypeToString = NewDBTypeConverter[exerr.ErrorType, string](func(v exerr.ErrorType) (string, error) { - return v.Key, nil -}, func(v string) (exerr.ErrorType, error) { - for _, etp := range exerr.ListRegisteredTypes() { - if etp.Key == v { - return etp, nil - } - } - - return exerr.NewType(v, nil), nil -}) - -type dbTypeConverterImpl[TModelData any, TDBData any] struct { +type dbTypeConverterImpl[TModelData any, TDBData DBDataConstraint] struct { dbTypeString string modelTypeString string todb func(v TModelData) (TDBData, error) @@ -212,7 +52,7 @@ func (t *dbTypeConverterImpl[TModelData, TDBData]) DBToModel(v any) (any, error) return nil, errors.New(fmt.Sprintf("Unexpected value in DBTypeConverter, expected '%s', found '%T'", t.dbTypeString, v)) } -func NewDBTypeConverter[TModelData any, TDBData any](todb func(v TModelData) (TDBData, error), tomodel func(v TDBData) (TModelData, error)) DBTypeConverter { +func NewDBTypeConverter[TModelData any, TDBData DBDataConstraint](todb func(v TModelData) (TDBData, error), tomodel func(v TDBData) (TModelData, error)) DBTypeConverter { return &dbTypeConverterImpl[TModelData, TDBData]{ dbTypeString: fmt.Sprintf("%T", *new(TDBData)), modelTypeString: fmt.Sprintf("%T", *new(TModelData)), @@ -221,6 +61,15 @@ func NewDBTypeConverter[TModelData any, TDBData any](todb func(v TModelData) (TD } } +func NewAutoDBTypeConverter[TDBData DBDataConstraint, TModelData DatabaseConvertible[TModelData, TDBData]](obj TModelData) DBTypeConverter { + return &dbTypeConverterImpl[TModelData, TDBData]{ + dbTypeString: fmt.Sprintf("%T", *new(TDBData)), + modelTypeString: fmt.Sprintf("%T", *new(TModelData)), + todb: obj.MarshalToDB, + tomodel: obj.UnmarshalToModel, + } +} + func convertValueToDB(q Queryable, value any) (any, error) { modelTypeStr := fmt.Sprintf("%T", value) diff --git a/sq/converterDefault.go b/sq/converterDefault.go new file mode 100644 index 0000000..4d7bd7e --- /dev/null +++ b/sq/converterDefault.go @@ -0,0 +1,161 @@ +package sq + +import ( + "errors" + "fmt" + "gogs.mikescher.com/BlackForestBytes/goext/exerr" + "gogs.mikescher.com/BlackForestBytes/goext/langext" + "gogs.mikescher.com/BlackForestBytes/goext/rfctime" + "gogs.mikescher.com/BlackForestBytes/goext/timeext" + "strconv" + "strings" + "time" +) + +// ========================== COMMON DATATYPES ========================== + +var ConverterBoolToBit = NewDBTypeConverter[bool, int64](func(v bool) (int64, error) { + return langext.Conditional(v, int64(1), int64(0)), nil +}, func(v int64) (bool, error) { + if v == 0 { + return false, nil + } + if v == 1 { + return true, nil + } + return false, errors.New(fmt.Sprintf("invalid valud for boolean: '%d'", v)) +}) + +var ConverterTimeToUnixMillis = NewDBTypeConverter[time.Time, int64](func(v time.Time) (int64, error) { + return v.UnixMilli(), nil +}, func(v int64) (time.Time, error) { + return time.UnixMilli(v), nil +}) + +// ========================== RFCTIME ========================== + +var ConverterRFCUnixMilliTimeToUnixMillis = NewDBTypeConverter[rfctime.UnixMilliTime, int64](func(v rfctime.UnixMilliTime) (int64, error) { + return v.UnixMilli(), nil +}, func(v int64) (rfctime.UnixMilliTime, error) { + return rfctime.NewUnixMilli(time.UnixMilli(v)), nil +}) + +var ConverterRFCUnixNanoTimeToUnixNanos = NewDBTypeConverter[rfctime.UnixNanoTime, int64](func(v rfctime.UnixNanoTime) (int64, error) { + return v.UnixNano(), nil +}, func(v int64) (rfctime.UnixNanoTime, error) { + return rfctime.NewUnixNano(time.Unix(0, v)), nil +}) + +var ConverterRFCUnixTimeToUnixSeconds = NewDBTypeConverter[rfctime.UnixTime, int64](func(v rfctime.UnixTime) (int64, error) { + return v.Unix(), nil +}, func(v int64) (rfctime.UnixTime, error) { + return rfctime.NewUnix(time.Unix(v, 0)), nil +}) + +// ConverterRFC339TimeToString +// Does not really use RFC339 - but sqlite does not understand timezones and the `T` delimiter +var ConverterRFC339TimeToString = NewDBTypeConverter[rfctime.RFC3339Time, string](func(v rfctime.RFC3339Time) (string, error) { + return v.Time().In(time.UTC).Format("2006-01-02 15:04:05"), nil +}, func(v string) (rfctime.RFC3339Time, error) { + t, err := time.Parse("2006-01-02 15:04:05", v) + if err != nil { + return rfctime.RFC3339Time{}, err + } + return rfctime.NewRFC3339(t), nil +}) + +// ConverterRFC339NanoTimeToString +// Does not really use RFC339 - but sqlite does not understand timezones and the `T` delimiter +var ConverterRFC339NanoTimeToString = NewDBTypeConverter[rfctime.RFC3339NanoTime, string](func(v rfctime.RFC3339NanoTime) (string, error) { + return v.Time().In(time.UTC).Format("2006-01-02 15:04:05.999999999"), nil +}, func(v string) (rfctime.RFC3339NanoTime, error) { + t, err := time.ParseInLocation("2006-01-02 15:04:05.999999999", v, time.UTC) + if err != nil { + return rfctime.RFC3339NanoTime{}, err + } + return rfctime.NewRFC3339Nano(t), nil +}) + +var ConverterRFCDateToString = NewDBTypeConverter[rfctime.Date, string](func(v rfctime.Date) (string, error) { + return fmt.Sprintf("%04d-%02d-%02d", v.Year, v.Month, v.Day), nil +}, func(v string) (rfctime.Date, error) { + split := strings.Split(v, "-") + if len(split) != 3 { + return rfctime.Date{}, errors.New("invalid date format: " + v) + } + year, err := strconv.ParseInt(split[0], 10, 32) + if err != nil { + return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) + } + month, err := strconv.ParseInt(split[0], 10, 32) + if err != nil { + return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) + } + day, err := strconv.ParseInt(split[0], 10, 32) + if err != nil { + return rfctime.Date{}, errors.New("invalid date format: " + v + ": " + err.Error()) + } + + return rfctime.Date{Year: int(year), Month: int(month), Day: int(day)}, nil +}) + +var ConverterRFCTimeToString = NewDBTypeConverter[rfctime.Time, string](func(v rfctime.Time) (string, error) { + return v.SerializeShort(), nil +}, func(v string) (rfctime.Time, error) { + res := rfctime.Time{} + err := res.Deserialize(v) + if err != nil { + return rfctime.Time{}, err + } + return res, nil +}) + +var ConverterRFCSecondsF64ToString = NewDBTypeConverter[rfctime.SecondsF64, float64](func(v rfctime.SecondsF64) (float64, error) { + return v.Seconds(), nil +}, func(v float64) (rfctime.SecondsF64, error) { + return rfctime.NewSecondsF64(timeext.FromSeconds(v)), nil +}) + +// ========================== JSON ========================== + +var ConverterJsonObjToString = NewAutoDBTypeConverter(JsonObj{}) + +var ConverterJsonArrToString = NewAutoDBTypeConverter(JsonArr{}) + +// Json[T] must be registered manually for each gen-type + +// ========================== EXERR ========================== + +var ConverterExErrCategoryToString = NewDBTypeConverter[exerr.ErrorCategory, string](func(v exerr.ErrorCategory) (string, error) { + return v.Category, nil +}, func(v string) (exerr.ErrorCategory, error) { + for _, cat := range exerr.AllCategories { + if cat.Category == v { + return cat, nil + } + } + return exerr.CatUser, errors.New("failed to convert '" + v + "' to exerr.ErrorCategory") +}) + +var ConverterExErrSeverityToString = NewDBTypeConverter[exerr.ErrorSeverity, string](func(v exerr.ErrorSeverity) (string, error) { + return v.Severity, nil +}, func(v string) (exerr.ErrorSeverity, error) { + for _, sev := range exerr.AllSeverities { + if sev.Severity == v { + return sev, nil + } + } + return exerr.SevErr, errors.New("failed to convert '" + v + "' to exerr.ErrorSeverity") +}) + +var ConverterExErrTypeToString = NewDBTypeConverter[exerr.ErrorType, string](func(v exerr.ErrorType) (string, error) { + return v.Key, nil +}, func(v string) (exerr.ErrorType, error) { + for _, etp := range exerr.ListRegisteredTypes() { + if etp.Key == v { + return etp, nil + } + } + + return exerr.NewType(v, nil), nil +}) diff --git a/sq/json.go b/sq/json.go index 4b66c36..215afdf 100644 --- a/sq/json.go +++ b/sq/json.go @@ -1,5 +1,59 @@ package sq +import "encoding/json" + type JsonObj map[string]any +func (j JsonObj) MarshalToDB(v JsonObj) (string, error) { + mrsh, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(mrsh), nil +} + +func (j JsonObj) UnmarshalToModel(v string) (JsonObj, error) { + var mrsh JsonObj + if err := json.Unmarshal([]byte(v), &mrsh); err != nil { + return JsonObj{}, err + } + return mrsh, nil +} + type JsonArr []any + +func (j JsonArr) MarshalToDB(v JsonArr) (string, error) { + mrsh, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(mrsh), nil +} + +func (j JsonArr) UnmarshalToModel(v string) (JsonArr, error) { + var mrsh JsonArr + if err := json.Unmarshal([]byte(v), &mrsh); err != nil { + return JsonArr{}, err + } + return mrsh, nil +} + +type AutoJson[T any] struct { + Value T +} + +func (j AutoJson[T]) MarshalToDB(v AutoJson[T]) (string, error) { + mrsh, err := json.Marshal(v.Value) + if err != nil { + return "", err + } + return string(mrsh), nil +} + +func (j AutoJson[T]) UnmarshalToModel(v string) (AutoJson[T], error) { + mrsh := *new(T) + if err := json.Unmarshal([]byte(v), &mrsh); err != nil { + return AutoJson[T]{}, err + } + return AutoJson[T]{Value: mrsh}, nil +}