Repository: surrealdb/surrealdb.go
Pull request #160 (2024-10-17 14:46:07)

Additional CBOR types
- Add Custom types with CBOR marshaling/unmarshaling
- Test and fix CBOR inconsistencies
- Add test

agufagit: Hi,

the returning result is `&[{Status:OK Time:28.641008ms Result:map[id:{Table:user ID:ls6bchgtj8l721a8dyeu}]}]`,
can you make returning result `&[{Status:OK Time:28.641008ms Result:map[id:user:ls6bchgtj8l721a8dyeu]}]`?  Which is compatible with old driver, and I don't see the use to make an ID an object, it's supposed to be a string.

Also I'm getting another error `"returnRawError":"json: unsupported type: map[interface {}]interface {}"`.

For `Query` function, the return type doesn't need to be a pointer, it can be `[]QueryResult[TResult]`, golang is different from rust, slices already contain a pointer, and are designed to be passed "as is" for data locality
remade: Hi @agufagit 

With CBOR implemented, all responses are CBOR encoded including the record ID. This is to our advantage. As @kearfy mentioned, there is a `String()` method on the RecordID type you can use to retrieve the familiar representation. Also, there are helper methods to create/parse record IDs and other custom types. As part of this PR, I will make sure all types have `String()` and `SurrealString()` methods
agufagit: Hi,

I hope this doesn't mean I have to loop every record to call id.String(), dateTime.toTime()? 

Suppose I have a struct

```go
type Data struct {
  ID string `json:"id"`
  Date time.Time `json:"date"`
}
```

will driver be smart enough to automatically convert id to string, date to time.Time? Or we have to do it ourselves?
remade: @agufagit 

Whenever you are retrieving the ID, when you retrieve the ID, you can simply call the string method
```
d.ID // for RecordID
d.ID.String() // for string representation
```
agufagit: that's what I want to avoid to do, my app is fairly complex, that would mean too much work.  I'm using auto gen graphql, where ID is string, I'll have to redefine every struct and map it to graphql struct instead of just using auto generated graphql struct

but no biggie, once you are done, I'll just fork the repo and make changes, and use that forked repo instead
remade: @agufagit 
Have you tried it out? When a string value is expected of any type, as far as it implements the `Stringer` interface, all should be well. You don't need to do anything extra. If you experience otherwise, it definitely worth resolving
agufagit: I didn't try the new commit yet, but that sounds great, I'll try it after you're done and new release is out for v2.0.0, thank you.

Patch:
diff --git a/db.go b/db.go
index 1b8561b..8fa75e2 100644
--- a/db.go
+++ b/db.go
@@ -201,7 +201,7 @@ func Query[TResult any](db *DB, sql string, vars map[string]interface{}) (*[]Que
 	return res.Result, nil
 }
 
-func Create[TResult any, TWhat models.TableOrRecord](db *DB, what TWhat, data interface{}) (*TResult, error) {
+func Create[TResult any, TWhat TableOrRecord](db *DB, what TWhat, data interface{}) (*TResult, error) {
 	var res connection.RPCResponse[TResult]
 	if err := db.con.Send(&res, "create", what, data); err != nil {
 		return nil, err
@@ -210,7 +210,7 @@ func Create[TResult any, TWhat models.TableOrRecord](db *DB, what TWhat, data in
 	return res.Result, nil
 }
 
-func Select[TResult any, TWhat models.TableOrRecord](db *DB, what TWhat) (*TResult, error) {
+func Select[TResult any, TWhat TableOrRecord](db *DB, what TWhat) (*TResult, error) {
 	var res connection.RPCResponse[TResult]
 
 	if err := db.con.Send(&res, "select", what); err != nil {
@@ -226,16 +226,16 @@ func Patch(db *DB, what interface{}, patches []PatchData) (*[]PatchData, error)
 	return patchRes.Result, err
 }
 
-func Delete[TWhat models.TableOrRecord](db *DB, what TWhat) error {
+func Delete[TWhat TableOrRecord](db *DB, what TWhat) error {
 	return db.con.Send(nil, "delete", what)
 }
 
-func Upsert[TWhat models.TableOrRecord](db *DB, what TWhat, data interface{}) error {
+func Upsert[TWhat TableOrRecord](db *DB, what TWhat, data interface{}) error {
 	return db.con.Send(nil, "upsert", what, data)
 }
 
 // Update a table or record in the database like a PUT request.
-func Update[TResult any, TWhat models.TableOrRecord](db *DB, what TWhat, data interface{}) (*TResult, error) {
+func Update[TResult any, TWhat TableOrRecord](db *DB, what TWhat, data interface{}) (*TResult, error) {
 	var res connection.RPCResponse[TResult]
 	if err := db.con.Send(&res, "update", what, data); err != nil {
 		return nil, err
diff --git a/pkg/models/cbor.go b/pkg/models/cbor.go
index 5531a68..1863feb 100644
--- a/pkg/models/cbor.go
+++ b/pkg/models/cbor.go
@@ -3,60 +3,68 @@ package models
 import (
 	"io"
 	"reflect"
-	"time"
 
 	"github.com/fxamacker/cbor/v2"
 	"github.com/surrealdb/surrealdb.go/internal/codec"
 )
 
-type CustomCBORTag uint64
-
 var (
-	NoneTag                 CustomCBORTag = 6
-	TableNameTag            CustomCBORTag = 7
-	RecordIDTag             CustomCBORTag = 8
-	UUIDStringTag           CustomCBORTag = 9
-	DecimalStringTag        CustomCBORTag = 10
-	DateTimeCompactString   CustomCBORTag = 12
-	DurationStringTag       CustomCBORTag = 13
-	DurationCompactTag      CustomCBORTag = 14
-	BinaryUUIDTag           CustomCBORTag = 37
-	GeometryPointTag        CustomCBORTag = 88
-	GeometryLineTag         CustomCBORTag = 89
-	GeometryPolygonTag      CustomCBORTag = 90
-	GeometryMultiPointTag   CustomCBORTag = 91
-	GeometryMultiLineTag    CustomCBORTag = 92
-	GeometryMultiPolygonTag CustomCBORTag = 93
-	GeometryCollectionTag   CustomCBORTag = 94
+	TagNone           uint64 = 6
+	TagTable          uint64 = 7
+	TagRecordID       uint64 = 8
+	TagCustomDatetime uint64 = 12
+	TagCustomDuration uint64 = 14
+	TagFuture         uint64 = 15
+
+	TagStringUUID     uint64 = 9
+	TagStringDecimal  uint64 = 10
+	TagStringDuration uint64 = 13
+
+	TagSpecBinaryUUID uint64 = 37
+
+	TagRange         uint64 = 49
+	TagBoundIncluded uint64 = 50
+	TagBoundExcluded uint64 = 51
+
+	TagGeometryPoint        uint64 = 88
+	TagGeometryLine         uint64 = 89
+	TagGeometryPolygon      uint64 = 90
+	TagGeometryMultiPoint   uint64 = 91
+	TagGeometryMultiLine    uint64 = 92
+	TagGeometryMultiPolygon uint64 = 93
+	TagGeometryCollection   uint64 = 94
 )
 
 func registerCborTags() cbor.TagSet {
-	customTags := map[CustomCBORTag]interface{}{
-		GeometryPointTag:        GeometryPoint{},
-		GeometryLineTag:         GeometryLine{},
-		GeometryPolygonTag:      GeometryPolygon{},
-		GeometryMultiPointTag:   GeometryMultiPoint{},
-		GeometryMultiLineTag:    GeometryMultiLine{},
-		GeometryMultiPolygonTag: GeometryMultiPolygon{},
-		GeometryCollectionTag:   GeometryCollection{},
-
-		TableNameTag: Table(""),
-		//UUIDStringTag:    UUID(""),
-		DecimalStringTag: Decimal(""),
-		BinaryUUIDTag:    UUID{},
-		NoneTag:          CustomNil{},
-
-		DateTimeCompactString: CustomDateTime(time.Now()),
-		DurationStringTag:     CustomDurationStr("2w"),
-		//DurationCompactTag:    CustomDuration(0),
+	customTags := map[uint64]interface{}{
+		TagNone:     CustomNil{},
+		TagTable:    Table(""),
+		TagRecordID: RecordID{},
+
+		TagCustomDatetime: CustomDateTime{},
+		TagCustomDuration: CustomDuration{},
+		TagFuture:         Future{},
+
+		TagStringUUID:     UUIDString(""),
+		TagStringDecimal:  DecimalString(""),
+		TagStringDuration: CustomDurationString(""),
+
+		TagSpecBinaryUUID: UUID{},
+
+		TagGeometryPoint:        GeometryPoint{},
+		TagGeometryLine:         GeometryLine{},
+		TagGeometryPolygon:      GeometryPolygon{},
+		TagGeometryMultiPoint:   GeometryMultiPoint{},
+		TagGeometryMultiLine:    GeometryMultiLine{},
+		TagGeometryMultiPolygon: GeometryMultiPolygon{},
+		TagGeometryCollection:   GeometryCollection{},
 	}
-
 	tags := cbor.NewTagSet()
 	for tag, customType := range customTags {
 		err := tags.Add(
 			cbor.TagOptions{EncTag: cbor.EncTagRequired, DecTag: cbor.DecTagRequired},
 			reflect.TypeOf(customType),
-			uint64(tag),
+			tag,
 		)
 		if err != nil {
 			panic(err)
diff --git a/pkg/models/datetime.go b/pkg/models/datetime.go
new file mode 100644
index 0000000..e9bdd12
--- /dev/null
+++ b/pkg/models/datetime.go
@@ -0,0 +1,54 @@
+package models
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/fxamacker/cbor/v2"
+	"github.com/surrealdb/surrealdb.go/pkg/constants"
+)
+
+// CustomDateTime embeds time.Time
+type CustomDateTime struct {
+	time.Time
+}
+
+func (d *CustomDateTime) MarshalCBOR() ([]byte, error) {
+	enc := getCborEncoder()
+
+	totalNS := d.Nanosecond()
+
+	s := totalNS / constants.OneSecondToNanoSecond
+	ns := totalNS % constants.OneSecondToNanoSecond
+
+	return enc.Marshal(cbor.Tag{
+		Number:  TagCustomDatetime,
+		Content: [2]int64{int64(s), int64(ns)},
+	})
+}
+
+func (d *CustomDateTime) UnmarshalCBOR(data []byte) error {
+	dec := getCborDecoder()
+
+	var temp [2]int64
+	err := dec.Unmarshal(data, &temp)
+	if err != nil {
+		return err
+	}
+
+	s := temp[0]
+	ns := temp[1]
+
+	*d = CustomDateTime{time.Unix(s, ns)}
+
+	return nil
+}
+
+func (d *CustomDateTime) String() string {
+	layout := "2006-01-02T15:04:05Z"
+	return d.Format(layout)
+}
+
+func (d *CustomDateTime) SurrealString() string {
+	return fmt.Sprintf("<datetime> '%s'", d.String())
+}
diff --git a/pkg/models/duration.go b/pkg/models/duration.go
index 2ba5aa3..a592a85 100644
--- a/pkg/models/duration.go
+++ b/pkg/models/duration.go
@@ -1,6 +1,9 @@
 package models
 
 import (
+	"fmt"
+	"regexp"
+	"strconv"
 	"time"
 
 	"github.com/surrealdb/surrealdb.go/pkg/constants"
@@ -8,19 +11,31 @@ import (
 	"github.com/fxamacker/cbor/v2"
 )
 
-type CustomDuration time.Duration
+const (
+	nsPerNanoSecond  = int64(1)
+	nsPerMicroSecond = 1000 * nsPerNanoSecond
+	nsPerMilliSecond = 1000 * nsPerMicroSecond
+	nsPerSecond      = 1000 * nsPerMilliSecond
+	nsPerMinute      = 60 * nsPerSecond
+	nsPerHour        = 60 * nsPerMinute
+	nsPerDay         = 24 * nsPerHour
+	nsPerWeek        = 7 * nsPerDay
+	nsPerYear        = 365 * nsPerDay
+)
 
-type CustomDurationStr string
+type CustomDuration struct {
+	time.Duration
+}
 
 func (d *CustomDuration) MarshalCBOR() ([]byte, error) {
 	enc := getCborEncoder()
 
-	totalNS := time.Duration(*d).Nanoseconds()
+	totalNS := d.Nanoseconds()
 	s := totalNS / constants.OneSecondToNanoSecond
 	ns := totalNS % constants.OneSecondToNanoSecond
 
 	return enc.Marshal(cbor.Tag{
-		Number:  uint64(DurationCompactTag),
+		Number:  TagCustomDuration,
 		Content: [2]int64{s, ns},
 	})
 }
@@ -37,7 +52,131 @@ func (d *CustomDuration) UnmarshalCBOR(data []byte) error {
 	s := temp[0].(int64)
 	ns := temp[1].(int64)
 
-	*d = CustomDuration(time.Duration((float64(s) * constants.OneSecondToNanoSecond) + float64(ns)))
+	*d = CustomDuration{time.Duration((float64(s) * constants.OneSecondToNanoSecond) + float64(ns))}
 
 	return nil
 }
+
+func (d *CustomDuration) String() string {
+	return FormatDuration(d.Nanoseconds())
+}
+
+func (d *CustomDuration) ToCustomDurationString() CustomDurationString {
+	return CustomDurationString(d.String())
+}
+
+//------------------------------------------------------------------------------------------------------------------------------//
+
+type CustomDurationString string
+
+func (d *CustomDurationString) String() string {
+	return string(*d)
+}
+
+func (d *CustomDurationString) ToDuration() time.Duration {
+	ns, err := ParseDuration(d.String())
+	if err != nil {
+		panic(err)
+	}
+
+	return time.Duration(ns)
+}
+
+func (d *CustomDurationString) ToCustomDuration() CustomDuration {
+	return CustomDuration{d.ToDuration()}
+}
+
+//------------------------------------------------------------------------------------------------------------------------------//
+
+func FormatDuration(ns int64) string {
+	years := ns / nsPerYear
+	ns %= nsPerYear
+
+	weeks := ns / nsPerWeek
+	ns %= nsPerWeek
+
+	days := ns / nsPerDay
+	ns %= nsPerDay
+
+	hours := ns / nsPerHour
+	ns %= nsPerHour
+
+	minutes := ns / nsPerMinute
+	ns %= nsPerMinute
+
+	seconds := ns / nsPerSecond
+	ns %= nsPerSecond
+
+	milliseconds := ns / nsPerMilliSecond
+	ns %= nsPerMilliSecond
+
+	microseconds := ns / nsPerMicroSecond
+	ns %= nsPerMicroSecond
+
+	result := ""
+	if years > 0 {
+		result += fmt.Sprintf("%dy", years)
+	}
+	if weeks > 0 {
+		result += fmt.Sprintf("%dw", weeks)
+	}
+	if days > 0 {
+		result += fmt.Sprintf("%dd", days)
+	}
+	if hours > 0 {
+		result += fmt.Sprintf("%dh", hours)
+	}
+	if minutes > 0 {
+		result += fmt.Sprintf("%dm", minutes)
+	}
+	if seconds > 0 {
+		result += fmt.Sprintf("%ds", seconds)
+	}
+	if milliseconds > 0 {
+		result += fmt.Sprintf("%dms", milliseconds)
+	}
+	if microseconds > 0 {
+		result += fmt.Sprintf("%dµs", microseconds)
+	}
+	if ns > 0 {
+		result += fmt.Sprintf("%dns", ns)
+	}
+
+	return result
+}
+
+func ParseDuration(duration string) (int64, error) {
+	// Regular expression to match the units in the duration string
+	re := regexp.MustCompile(`(\d+)([a-zµ]+)`)
+	matches := re.FindAllStringSubmatch(duration, -1)
+
+	var totalNanoseconds int64
+
+	for _, match := range matches {
+		value, _ := strconv.ParseInt(match[1], 10, 64)
+		unit := match[2]
+
+		switch unit {
+		case "y":
+			totalNanoseconds += value * nsPerYear
+		case "w":
+			totalNanoseconds += value * nsPerWeek
+		case "d":
+			totalNanoseconds += value * nsPerDay
+		case "h":
+			totalNanoseconds += value * nsPerHour
+		case "m":
+			totalNanoseconds += value * nsPerMinute
+		case "s":
+			totalNanoseconds += value * nsPerSecond
+		case "ms":
+			totalNanoseconds += value * nsPerMilliSecond
+		case "µs", "us":
+			totalNanoseconds += value * nsPerMicroSecond
+		case "ns":
+			totalNanoseconds += value * nsPerNanoSecond
+		}
+	}
+
+	return totalNanoseconds, nil
+}
diff --git a/pkg/models/future.go b/pkg/models/future.go
new file mode 100644
index 0000000..6548e32
--- /dev/null
+++ b/pkg/models/future.go
@@ -0,0 +1,15 @@
+package models
+
+import "fmt"
+
+type Future struct {
+	inner string
+}
+
+func (f *Future) String() string {
+	return f.inner
+}
+
+func (f *Future) SurrealString() string {
+	return fmt.Sprintf("<future> { %s }", f.String())
+}
diff --git a/pkg/models/geometry.go b/pkg/models/geometry.go
index cbed82e..ad4fd4a 100644
--- a/pkg/models/geometry.go
+++ b/pkg/models/geometry.go
@@ -21,7 +21,7 @@ func (gp *GeometryPoint) MarshalCBOR() ([]byte, error) {
 	enc := getCborEncoder()
 
 	return enc.Marshal(cbor.Tag{
-		Number:  uint64(GeometryPointTag),
+		Number:  TagGeometryPoint,
 		Content: gp.GetCoordinates(),
 	})
 }
diff --git a/pkg/models/range.go b/pkg/models/range.go
new file mode 100644
index 0000000..047944f
--- /dev/null
+++ b/pkg/models/range.go
@@ -0,0 +1,149 @@
+package models
+
+import (
+	"fmt"
+	"reflect"
+
+	"github.com/fxamacker/cbor/v2"
+)
+
+type BoundIncluded[T any] struct {
+	Value T
+}
+
+func (bi *BoundIncluded[T]) MarshalCBOR() ([]byte, error) {
+	return getCborEncoder().Marshal(cbor.Tag{
+		Number:  TagBoundIncluded,
+		Content: bi.Value,
+	})
+}
+
+func (bi *BoundIncluded[T]) UnmarshalCBOR(data []byte) error {
+	var temp T
+	err := getCborDecoder().Unmarshal(data, &temp)
+	if err != nil {
+		return err
+	}
+
+	bi.Value = temp
+	return nil
+}
+
+//------------------------------------------------------------------------------------------------//
+
+type BoundExcluded[T any] struct {
+	Value T
+}
+
+func (be *BoundExcluded[T]) MarshalCBOR() ([]byte, error) {
+	return getCborEncoder().Marshal(cbor.Tag{
+		Number:  TagBoundExcluded,
+		Content: be.Value,
+	})
+}
+
+func (be *BoundExcluded[T]) UnmarshalCBOR(data []byte) error {
+	var temp T
+	err := getCborDecoder().Unmarshal(data, &temp)
+	if err != nil {
+		return err
+	}
+
+	be.Value = temp
+	return nil
+}
+
+//------------------------------------------------------------------------------------------------//
+
+type Bound[T any] interface {
+	BoundIncluded[T] | BoundExcluded[T]
+}
+
+type Range[T any, TBeg Bound[T], TEnd Bound[T]] struct {
+	Begin *TBeg
+	End   *TEnd
+}
+
+func (r *Range[T, TBeg, TEnd]) GetJoinString() string {
+	joinStr := ""
+
+	if reflect.TypeOf(*r.Begin) == reflect.TypeOf(BoundExcluded[T]{}) {
+		joinStr += ">"
+	}
+	joinStr += ".."
+	if reflect.TypeOf(*r.End) == reflect.TypeOf(BoundIncluded[T]{}) {
+		joinStr += "="
+	}
+
+	return joinStr
+}
+
+func (r *Range[T, TBeg, TEnd]) String() string {
+	joinStr := r.GetJoinString()
+	beginStr := ""
+	endStr := ""
+
+	if r.Begin != nil {
+		beginStr = convertToString(r.Begin)
+	}
+	if r.End != nil {
+		endStr = convertToString(r.Begin)
+	}
+
+	return fmt.Sprintf("%s%s%s", beginStr, joinStr, endStr)
+}
+
+func (r *Range[T, TBeg, TEnd]) MarshalCBOR() ([]byte, error) {
+	return getCborEncoder().Marshal(cbor.Tag{
+		Number:  TagRange,
+		Content: []interface{}{r.Begin, r.End},
+	})
+}
+
+func (r *Range[T, TBeg, TEnd]) UnmarshalCBOR(data []byte) error {
+	dec := getCborDecoder()
+	var temp [2]cbor.RawTag
+	err := getCborDecoder().Unmarshal(data, &temp)
+	if err != nil {
+		return err
+	}
+
+	var begin TBeg
+	beginEnc, _ := temp[0].MarshalCBOR()
+	_ = dec.Unmarshal(beginEnc, &begin)
+
+	var end TEnd
+	endEnc, _ := temp[1].MarshalCBOR()
+	_ = dec.Unmarshal(endEnc, &end)
+
+	r.Begin = &begin
+	r.End = &end
+	return nil
+}
+
+//---------------------------------------------------------------------------------------------------------------------//
+
+type RecordRangeID[T any, TBeg Bound[T], TEnd Bound[T]] struct {
+	Range[T, TBeg, TEnd]
+	Table Table
+}
+
+func (rr *RecordRangeID[T, TBeg, TEnd]) String() string {
+	joinStr := rr.GetJoinString()
+	beginStr := ""
+	endStr := ""
+
+	if rr.Begin != nil {
+		beginStr = convertToString(rr.Begin)
+	}
+	if rr.End != nil {
+		endStr = convertToString(rr.Begin)
+	}
+
+	return fmt.Sprintf("%s:%s%s%s", rr.Table, beginStr, joinStr, endStr)
+}
+
+func convertToString(v any) string {
+	// todo: implement
+	return ""
+}
diff --git a/pkg/models/record_id.go b/pkg/models/record_id.go
index a52f31e..cb466e5 100644
--- a/pkg/models/record_id.go
+++ b/pkg/models/record_id.go
@@ -35,7 +35,7 @@ func (r *RecordID) MarshalCBOR() ([]byte, error) {
 	enc := getCborEncoder()
 
 	return enc.Marshal(cbor.Tag{
-		Number:  uint64(RecordIDTag),
+		Number:  TagRecordID,
 		Content: []interface{}{r.Table, r.ID},
 	})
 }
@@ -58,3 +58,7 @@ func (r *RecordID) UnmarshalCBOR(data []byte) error {
 func (r *RecordID) String() string {
 	return fmt.Sprintf("%s:%s", r.Table, r.ID)
 }
+
+func (r *RecordID) SurrealString() string {
+	return fmt.Sprintf("r'%s'", r.String())
+}
diff --git a/pkg/models/replacer.go b/pkg/models/replacer.go
index 3bb1c08..0322a9b 100644
--- a/pkg/models/replacer.go
+++ b/pkg/models/replacer.go
@@ -11,7 +11,7 @@ func replacerBeforeEncode(value interface{}) interface{} {
 
 	if valueType == reflect.TypeOf(time.Duration(0)) {
 		oldVal := value.(time.Duration)
-		newValue := CustomDuration(oldVal.Nanoseconds())
+		newValue := CustomDuration{oldVal}
 		return newValue
 	}
 
@@ -38,9 +38,9 @@ func replacerAfterDecode(value interface{}) interface{} {
 	valueType := reflect.TypeOf(value)
 	valueKind := valueType.Kind()
 
-	if valueType == reflect.TypeOf(CustomDuration(0)) {
+	if valueType == reflect.TypeOf(CustomDuration{}) {
 		oldVal := value.(CustomDuration)
-		newValue := time.Duration(oldVal)
+		newValue := oldVal.Duration
 		return newValue
 	}
 
diff --git a/pkg/models/table.go b/pkg/models/table.go
new file mode 100644
index 0000000..9068740
--- /dev/null
+++ b/pkg/models/table.go
@@ -0,0 +1,7 @@
+package models
+
+type Table string
+
+func (t Table) String() string {
+	return string(t)
+}
diff --git a/pkg/models/types.go b/pkg/models/types.go
index bd34f03..b97078e 100644
--- a/pkg/models/types.go
+++ b/pkg/models/types.go
@@ -1,61 +1,10 @@
 package models
 
 import (
-	"time"
-
-	"github.com/surrealdb/surrealdb.go/pkg/constants"
-
 	"github.com/fxamacker/cbor/v2"
-	"github.com/gofrs/uuid"
 )
 
-type TableOrRecord interface {
-	string | Table | RecordID | []Table | []RecordID
-}
-
-type Table string
-
-// type UUID string
-
-// type UUIDBin []byte
-type UUID struct {
-	uuid.UUID
-}
-
-type Decimal string
-
-type CustomDateTime time.Time
-
-func (d *CustomDateTime) MarshalCBOR() ([]byte, error) {
-	enc := getCborEncoder()
-
-	totalNS := time.Time(*d).Nanosecond()
-
-	s := totalNS / constants.OneSecondToNanoSecond
-	ns := totalNS % constants.OneSecondToNanoSecond
-
-	return enc.Marshal(cbor.Tag{
-		Number:  uint64(DateTimeCompactString),
-		Content: [2]int64{int64(s), int64(ns)},
-	})
-}
-
-func (d *CustomDateTime) UnmarshalCBOR(data []byte) error {
-	dec := getCborDecoder()
-
-	var temp [2]int64
-	err := dec.Unmarshal(data, &temp)
-	if err != nil {
-		return err
-	}
-
-	s := temp[0]
-	ns := temp[1]
-
-	*d = CustomDateTime(time.Unix(s, ns))
-
-	return nil
-}
+type DecimalString string
 
 type CustomNil struct {
 }
@@ -64,7 +13,7 @@ func (c *CustomNil) MarshalCBOR() ([]byte, error) {
 	enc := getCborEncoder()
 
 	return enc.Marshal(cbor.Tag{
-		Number:  uint64(NoneTag),
+		Number:  TagNone,
 		Content: nil,
 	})
 }
diff --git a/pkg/models/uuid.go b/pkg/models/uuid.go
new file mode 100644
index 0000000..1b35edd
--- /dev/null
+++ b/pkg/models/uuid.go
@@ -0,0 +1,9 @@
+package models
+
+import "github.com/gofrs/uuid"
+
+type UUIDString string
+
+type UUID struct {
+	uuid.UUID
+}
diff --git a/types.go b/types.go
index c6340ee..2773723 100644
--- a/types.go
+++ b/types.go
@@ -56,3 +56,7 @@ type Obj map[interface{}]interface{}
 type Result[T any] struct {
 	T any
 }
+
+type TableOrRecord interface {
+	string | models.Table | models.RecordID | []models.Table | []models.RecordID
+}


Test patch:
diff --git a/pkg/models/cbor_test.go b/pkg/models/cbor_test.go
index 85150bc..dd8bf27 100644
--- a/pkg/models/cbor_test.go
+++ b/pkg/models/cbor_test.go
@@ -68,14 +68,14 @@ func TestForRequestPayload(t *testing.T) {
 	params := []interface{}{
 		"SELECT marketing, count() FROM $tb GROUP BY marketing",
 		map[string]interface{}{
-			"tb":       Table("person"),
-			"line":     GeometryLine{NewGeometryPoint(11.11, 22.22), NewGeometryPoint(33.33, 44.44)},
-			"datetime": time.Now(),
-			"testNone": None,
-			"testNil":  nil,
-			"duration": time.Duration(340),
-			// "custom_duration": CustomDuration(340),
-			"custom_datetime": CustomDateTime(time.Now()),
+			"tb":              Table("person"),
+			"line":            GeometryLine{NewGeometryPoint(11.11, 22.22), NewGeometryPoint(33.33, 44.44)},
+			"datetime":        time.Now(),
+			"testNone":        None,
+			"testNil":         nil,
+			"duration":        time.Duration(340),
+			"custom_duration": CustomDuration{340},
+			"custom_datetime": CustomDateTime{time.Now()},
 		},
 	}
 
@@ -94,3 +94,118 @@ func TestForRequestPayload(t *testing.T) {
 
 	fmt.Println(diagStr)
 }
+
+func TestRange_GetJoinString(t *testing.T) {
+	t.Run("begin excluded, end excluded", func(s *testing.T) {
+		r := &Range[int, BoundExcluded[int], BoundExcluded[int]]{
+			Begin: &BoundExcluded[int]{0},
+			End:   &BoundExcluded[int]{10},
+		}
+		assert.Equal(t, ">..", r.GetJoinString())
+	})
+
+	t.Run("begin excluded, end included", func(t *testing.T) {
+		r := Range[int, BoundExcluded[int], BoundIncluded[int]]{
+			Begin: &BoundExcluded[int]{0},
+			End:   &BoundIncluded[int]{10},
+		}
+		assert.Equal(t, ">..=", r.GetJoinString())
+	})
+
+	t.Run("begin included, end excluded", func(t *testing.T) {
+		r := Range[int, BoundIncluded[int], BoundExcluded[int]]{
+			Begin: &BoundIncluded[int]{0},
+			End:   &BoundExcluded[int]{10},
+		}
+		assert.Equal(t, "..", r.GetJoinString())
+	})
+
+	t.Run("begin included, end included", func(t *testing.T) {
+		r := Range[int, BoundIncluded[int], BoundIncluded[int]]{
+			Begin: &BoundIncluded[int]{0},
+			End:   &BoundIncluded[int]{10},
+		}
+		assert.Equal(t, "..=", r.GetJoinString())
+	})
+}
+
+func TestRange_Bounds(t *testing.T) {
+	em := getCborEncoder()
+	dm := getCborDecoder()
+
+	t.Run("bound included should be marshaled and unmarshaled properly", func(t *testing.T) {
+		bi := BoundIncluded[int]{10}
+		encoded, err := em.Marshal(bi)
+		assert.NoError(t, err)
+
+		var decoded BoundIncluded[int]
+		err = dm.Unmarshal(encoded, &decoded)
+		assert.NoError(t, err)
+		assert.Equal(t, bi, decoded)
+	})
+
+	t.Run("bound excluded should be marshaled and unmarshaled properly", func(t *testing.T) {
+		be := BoundExcluded[int]{10}
+		encoded, err := em.Marshal(be)
+		assert.NoError(t, err)
+
+		var decoded BoundExcluded[int]
+		err = dm.Unmarshal(encoded, &decoded)
+		assert.NoError(t, err)
+		assert.Equal(t, be, decoded)
+	})
+}
+
+func TestRange_CODEC(t *testing.T) {
+	em := getCborEncoder()
+	dm := getCborDecoder()
+
+	r := Range[int, BoundIncluded[int], BoundExcluded[int]]{
+		Begin: &BoundIncluded[int]{0},
+		End:   &BoundExcluded[int]{10},
+	}
+
+	encoded, err := em.Marshal(r)
+	assert.NoError(t, err)
+
+	var decoded Range[int, BoundIncluded[int], BoundExcluded[int]]
+	err = dm.Unmarshal(encoded, &decoded)
+	assert.NoError(t, err)
+	assert.Equal(t, r, decoded)
+}
+
+func TestCustomDateTime_String(t *testing.T) {
+	time1, err := time.Parse("2006-01-02 15:04:05", "2024-10-30 12:05:00")
+	assert.NoError(t, err)
+
+	cd := CustomDateTime{time1}
+	assert.Equal(t, "2024-10-30T12:05:00Z", cd.String())
+}
+
+func TestTable_String(t *testing.T) {
+	table := Table("mytesttable")
+	assert.Equal(t, "mytesttable", table.String())
+}
+
+func TestCustomDuration_String(t *testing.T) {
+	cd := CustomDuration{time.Duration(33333333333000000)}
+	assert.Equal(t, "1y2w6d19h15m33s333ms", cd.String())
+}
+
+func TestRecordID_String(t *testing.T) {
+	rid := RecordID{Table: "mytesttable", ID: "121212121"}
+	assert.Equal(t, "mytesttable:121212121", rid.String())
+}
+
+func TestFormatDurationAndParseDuration(t *testing.T) {
+	durationStr := "1y2w6d19h15m33s333ms"
+
+	ns, _ := ParseDuration(durationStr)
+	d := FormatDuration(ns)
+	assert.Equal(t, durationStr, d)
+}
+
+func TestFormatDuration(t *testing.T) {
+	d := FormatDuration(33333333333000000)
+	assert.Equal(t, "1y2w6d19h15m33s333ms", d)
+}