Skip to content

Commit

Permalink
support time and duration everywhere
Browse files Browse the repository at this point in the history
  • Loading branch information
NoBypass committed May 18, 2024
1 parent 26eaf46 commit 49d1f5a
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 51 deletions.
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# SurGo
A simple sqlx-like library for using SurrealDB in Go.

## Table of Contents
* [Table of Contents](#table-of-contents)
**Table of Contents**
* [Installation](#installation)
* [Connecting to a database](#connecting-to-a-database)
* [Configure the connection](#configure-the-connection)
Expand Down Expand Up @@ -140,8 +139,8 @@ type User struct {
Age int
}
result, err := db.Exec("INSERT INTO users (name, age) VALUES ($name, $age)", User{
Name: "John",
Age: 25,
Name: "John",
Age: 25,
})
```

Expand Down Expand Up @@ -203,8 +202,14 @@ db.Exec("RELATE foo:$->edge->bar:$", surgo.ID{123}, surgo.ID{456}
// will be parsed to: RELATE foo:123->edge->bar:456
```

## Contributing
Just make a pull request, and it will be reviewed as soon as possible.
### Datetimes and Durations
You can use the `time.Time` type for datetime values. The library will automatically convert them to the correct format
for SurrealDB. Some goes for the `time.Duration` type. It will be converted to the highest possible unit (e.g. seconds)
that keeps the precision. Both of these types can be used as parameters in query functions. It does not matter if they are used as normal values,
in maps, or in structs. When scanning if the destination field is of type `time.Time` and the source is a string, the library will try to parse
the string to a `time.Time` object.


## To-Do
- Improve error messages
- Improve error messages/errors in general
- Use variables in ranged IDs
1 change: 1 addition & 0 deletions actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (db *DB) Exec(query string, args ...any) ([]Result, error) {
if err != nil {
return nil, err
}

ids, ok := params["$"]
if ok {
for _, id := range ids.([]any) {
Expand Down
60 changes: 36 additions & 24 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@ const (
)

type (
ID []any
Range [2]ID
Datetime Date
Date struct {
time.Time
}
ID []any
Range [2]ID
)

func (id ID) string() string {
Expand All @@ -25,10 +21,8 @@ func (id ID) string() string {
switch v.(type) {
case string:
return fmt.Sprintf("`%s`", strings.Replace(id[0].(string), "`", "", -1))
case Datetime:
return fmt.Sprintf("`%s`", v.(Datetime).string())
case Date:
return fmt.Sprintf("`%s`", v.(Date).string())
case time.Time:
return fmt.Sprintf("`%s`", v.(time.Time).Format(time.RFC3339))
default:
return fmt.Sprintf("%v", id[0])
}
Expand All @@ -39,10 +33,8 @@ func (id ID) string() string {
switch v.(type) {
case string:
v = fmt.Sprintf("'%s'", strings.Replace(v.(string), "'", "", -1))
case Datetime:
v = rangedString(v.(Datetime).string())
case Date:
v = rangedString(v.(Date).string())
case time.Time:
v = fmt.Sprintf("<datetime>'%s'", v.(time.Time).Format(time.RFC3339))
default:
v = fmt.Sprintf("%v", v)
}
Expand All @@ -56,14 +48,34 @@ func (r Range) string() string {
return fmt.Sprintf("%s..%s", r[0].string(), r[1].string())
}

func (dt Datetime) string() string {
return dt.Format(time.RFC3339)
}

func (dt Date) string() string {
return dt.Format(time.DateOnly)
}

func rangedString(dt string) string {
return fmt.Sprintf("<datetime>'%s'", dt)
func parseTimes(ts any) (string, bool) {
switch ts.(type) {
case time.Time:
return ts.(time.Time).Format(time.RFC3339), true
case time.Duration:
total := ts.(time.Duration)
var unit string
switch {
case total >= time.Hour:
total = total / time.Hour
unit = "h"
case total >= time.Minute:
total = total / time.Minute
unit = "m"
case total >= time.Second:
total = total / time.Second
unit = "s"
case total >= time.Millisecond:
total = total / time.Millisecond
unit = "ms"
case total >= time.Microsecond:
total = total / time.Microsecond
unit = "us"
default:
total = total / time.Nanosecond
unit = "ns"
}
return fmt.Sprintf("%d%s", total, unit), true
}
return "", false
}
31 changes: 28 additions & 3 deletions parse_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,36 @@ func parseParam(arg any, idx *int) (map[string]any, error) {
case Range:
m["$"] = []any{arg}
return m, nil
default:
t, ok := parseTimes(arg)
if ok {
m[fmt.Sprintf("%d", *idx+1)] = t
*idx++
return m, nil
}
}

v := reflect.ValueOf(arg)
switch v.Kind() {
case reflect.Map:
return arg.(map[string]any), nil
m := arg.(map[string]any)
for k, v := range m {
t, ok := parseTimes(v)
if ok {
m[k] = t
}
}
return m, nil
case reflect.Struct:
return structToMap(arg), nil
default:
m[fmt.Sprintf("%d", *idx+1)] = v.Interface()
var val any
val, ok := parseTimes(arg)
if !ok {
val = v.Interface()
}

m[fmt.Sprintf("%d", *idx+1)] = val
*idx++
return m, nil
}
Expand All @@ -79,7 +99,12 @@ func structToMap[T any](content T) map[string]any {
name = tag
}

m[name] = reflect.ValueOf(content).Field(i).Interface()
v := reflect.ValueOf(content).Field(i).Interface()
ts, ok := parseTimes(v)
if ok {
v = ts
}
m[name] = v
}

return m
Expand Down
37 changes: 20 additions & 17 deletions parse_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,26 +247,14 @@ func Test_parseQuery(t *testing.T) {
"1": 1,
})
})
t.Run("Query with a date id", func(t *testing.T) {
m := new(MockQueryAgent)
db := &DB{m}
query := "SELECT * FROM test:$"

m.On("Query", mock.Anything, mock.Anything).Return(emptyResponse, nil)
now := time.Now()
_, err := db.Exec(query, ID{Date{now}})

assert.NoError(t, err)
m.AssertCalled(t, "Query", fmt.Sprintf("SELECT * FROM test:`%s`;", now.Format(time.DateOnly)), map[string]any{})
})
t.Run("Query with a datetime id", func(t *testing.T) {
m := new(MockQueryAgent)
db := &DB{m}
query := "SELECT * FROM test:$"

m.On("Query", mock.Anything, mock.Anything).Return(emptyResponse, nil)
now := time.Now()
_, err := db.Exec(query, ID{Datetime{now}})
_, err := db.Exec(query, ID{now})

assert.NoError(t, err)
m.AssertCalled(t, "Query", fmt.Sprintf("SELECT * FROM test:`%s`;", now.Format(time.RFC3339)), map[string]any{})
Expand All @@ -279,16 +267,31 @@ func Test_parseQuery(t *testing.T) {
m.On("Query", mock.Anything, mock.Anything).Return(emptyResponse, nil)
now := time.Now()
_, err := db.Exec(query, Range{
ID{"test", Date{now}},
ID{"test", Date{now.AddDate(0, 0, 1)}},
ID{"test", now},
ID{"test", now.AddDate(0, 0, 1)},
})

assert.NoError(t, err)
m.AssertCalled(t, "Query",
fmt.Sprintf("SELECT * FROM test:['test', <datetime>'%s']..['test', <datetime>'%s'];",
now.Format(time.DateOnly),
now.AddDate(0, 0, 1).Format(time.DateOnly)),
now.Format(time.RFC3339),
now.AddDate(0, 0, 1).Format(time.RFC3339)),
map[string]any{},
)
})
t.Run("Query with a time and duration", func(t *testing.T) {
m := new(MockQueryAgent)
db := &DB{m}
query := "CREATE test SET time = $1 + $2"

m.On("Query", mock.Anything, mock.Anything).Return(emptyResponse, nil)
now := time.Now()
_, err := db.Exec(query, now, time.Hour)

assert.NoError(t, err)
m.AssertCalled(t, "Query", "CREATE test SET time = $1 + $2;", map[string]any{
"1": now.Format(time.RFC3339),
"2": "1h",
})
})
}
9 changes: 9 additions & 0 deletions scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"reflect"
"strings"
"time"
)

func scan(src any, dest any) error {
Expand Down Expand Up @@ -95,6 +96,14 @@ func parseValue(srcVal reflect.Value, destVal reflect.Value) error {
if destVal.Kind() == reflect.Int && srcVal.Kind() == reflect.Float64 {
destVal.SetInt(int64(srcVal.Float()))
return nil
} else if destVal.Type() == reflect.TypeOf(time.Time{}) && srcVal.Kind() == reflect.String {
t, err := time.Parse(time.RFC3339, srcVal.String())
if err != nil {
return err
}

destVal.Set(reflect.ValueOf(t))
return nil
}

return fmt.Errorf("cannot assign %s to %s", srcVal.Type(), destVal.Type())
Expand Down
17 changes: 17 additions & 0 deletions scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package surgo
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)

type testStruct struct {
Expand All @@ -12,6 +13,10 @@ type testStruct struct {
Age float64
}

type testStructWithTime struct {
Time time.Time
}

type nestedTestStruct struct {
Title string
Test testStruct
Expand Down Expand Up @@ -246,4 +251,16 @@ func Test_scan(t *testing.T) {
Test: nil,
}, s)
})
t.Run("scan to time.Time field", func(t *testing.T) {
m := map[string]any{
"time": "2011-01-01T00:00:00Z",
}

var s testStructWithTime
err := scan(m, &s)
assert.NoError(t, err)
assert.Equal(t, testStructWithTime{
Time: time.Date(2011, 1, 1, 0, 0, 0, 0, time.UTC),
}, s)
})
}

0 comments on commit 49d1f5a

Please sign in to comment.