Skip to content

Commit

Permalink
Merge pull request #30 from moznion/sql_interface
Browse files Browse the repository at this point in the history
Implements database/sql/driver.Valuer and database/sql.Scanner on Option type
  • Loading branch information
moznion committed Jul 22, 2023
2 parents 30a9e7f + c07ad7f commit 1544d90
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 20 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,40 @@ if err != nil {
fmt.Printf("%s\n", marshal) // => {}
```

### SQL Driver Support

`Option[T]` satisfies [sql/driver.Valuer](https://pkg.go.dev/database/sql/driver#Valuer) and [sql.Scanner](https://pkg.go.dev/database/sql#Scanner), so this type can be used by SQL interface on Golang.

example of the primitive usage:

```go
sqlStmt := "CREATE TABLE tbl (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(32));"
db.Exec(sqlStmt)

tx, _ := db.Begin()
func() {
stmt, _ := tx.Prepare("INSERT INTO tbl(id, name) values(?, ?)")
defer stmt.Close()
stmt.Exec(1, "foo")
}()
func() {
stmt, _ := tx.Prepare("INSERT INTO tbl(id) values(?)")
defer stmt.Close()
stmt.Exec(2) // name is NULL
}()
tx.Commit()

var maybeName Option[string]

row := db.QueryRow("SELECT name FROM tbl WHERE id = 1")
row.Scan(&maybeName)
fmt.Println(maybeName) // Some[foo]

row := db.QueryRow("SELECT name FROM tbl WHERE id = 2")
row.Scan(&maybeName)
fmt.Println(maybeName) // None[]
```

## Known Issues

The runtime raises a compile error like "methods cannot have type parameters", so `Map()`, `MapOr()`, `MapWithError()`, `MapOrWithError()`, `Zip()`, `ZipWith()`, `Unzip()` and `UnzipWith()` have been providing as functions. Basically, it would be better to provide them as the methods, but currently, it compromises with the limitation.
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/moznion/go-optional

go 1.19

require github.com/stretchr/testify v1.8.4
require (
github.com/mattn/go-sqlite3 v1.14.17
github.com/stretchr/testify v1.8.4
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down
21 changes: 2 additions & 19 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1 change: 1 addition & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var (
)

// Option is a data type that must be Some (i.e. having a value) or None (i.e. doesn't have a value).
// This type implements database/sql/driver.Valuer and database/sql.Scanner.
type Option[T any] []T

const (
Expand Down
46 changes: 46 additions & 0 deletions sql_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package optional

import (
"database/sql/driver"
"errors"
"time"
)

var (
ErrSQLScannerIncompatibleDataType = errors.New("incompatible data type for SQL scanner on Option[T]")
ErrSQLDriverValuerIncompatibleDataType = errors.New("incompatible data type for SQL driver Valuer on Option[T]")
)

// Scan assigns a value from a database driver.
// This method is required from database/sql.Scanner interface.
func (o *Option[T]) Scan(src any) error {
if src == nil {
*o = None[T]()
return nil
}

switch src.(type) {
case string, []byte, int64, float64, bool, time.Time:
*o = Some[T](src.(T))
default:
return ErrSQLScannerIncompatibleDataType
}

return nil
}

// Value returns a driver Value.
// This method is required from database/sql/driver.Valuer interface.
func (o Option[T]) Value() (driver.Value, error) {
if o.IsNone() {
return nil, nil
}

v := o.Unwrap()
switch (interface{})(v).(type) {
case string, []byte, int64, float64, bool, time.Time:
return v, nil
default:
return nil, ErrSQLDriverValuerIncompatibleDataType
}
}
176 changes: 176 additions & 0 deletions sql_driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package optional

import (
"database/sql"
"database/sql/driver"
"os"
"testing"
"time"

_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
)

func TestOption_Scan(t *testing.T) {
o := Some[any](nil)

err := o.Scan("bar")
assert.NoError(t, err)
assert.EqualValues(t, "bar", o.Unwrap())

err = o.Scan([]byte("buz"))
assert.NoError(t, err)
assert.EqualValues(t, []byte("buz"), o.Unwrap())

err = o.Scan(int64(42))
assert.NoError(t, err)
assert.EqualValues(t, 42, o.Unwrap())

err = o.Scan(float64(123.456))
assert.NoError(t, err)
assert.EqualValues(t, 123.456, o.Unwrap())

err = o.Scan(true)
assert.NoError(t, err)
assert.EqualValues(t, true, o.Unwrap())

now := time.Now()
err = o.Scan(now)
assert.NoError(t, err)
assert.EqualValues(t, now, o.Unwrap())
}

func TestOption_Scan_None(t *testing.T) {
o := Some[any](nil)

err := o.Scan(nil)
assert.NoError(t, err)
assert.True(t, o.IsNone())
}

func TestOption_Scan_UnsupportedTypes(t *testing.T) {
o := Some[any](nil)

err := o.Scan(int32(42))
assert.ErrorIs(t, err, ErrSQLScannerIncompatibleDataType)
}

func TestOption_Scan_ScannerInterfaceSatisfaction(t *testing.T) {
o := Some[any]("string")
var s sql.Scanner = &o
assert.NotNil(t, s)
}

func TestOption_Value(t *testing.T) {
{
o := Some[string]("foo")
v, err := o.Value()
assert.NoError(t, err)
assert.EqualValues(t, "foo", v)
}

{
o := Some[[]byte]([]byte("bar"))
v, err := o.Value()
assert.NoError(t, err)
assert.EqualValues(t, []byte("bar"), v)
}

{
o := Some[int64](42)
v, err := o.Value()
assert.NoError(t, err)
assert.EqualValues(t, 42, v)
}

{
o := Some[float64](123.456)
v, err := o.Value()
assert.NoError(t, err)
assert.EqualValues(t, 123.456, v)
}

{
o := Some[bool](true)
v, err := o.Value()
assert.NoError(t, err)
assert.EqualValues(t, true, v)
}

{
now := time.Now()
o := Some[time.Time](now)
v, err := o.Value()
assert.NoError(t, err)
assert.EqualValues(t, now, v)
}
}

func TestOption_Value_None(t *testing.T) {
o := None[string]()
v, err := o.Value()
assert.NoError(t, err)
assert.Nil(t, v)
}

func TestOption_Value_UnsupportedTypes(t *testing.T) {
o := Some[int32](0)
_, err := o.Value()
assert.ErrorIs(t, err, ErrSQLDriverValuerIncompatibleDataType)
}

func TestOption_Value_ValuerInterfaceSatisfaction(t *testing.T) {
o := Some[any]("string")
var s driver.Valuer = &o
assert.NotNil(t, s)
}

func TestOption_SQLScan(t *testing.T) {
tmpfile, err := os.CreateTemp(os.TempDir(), "testdb")
assert.NoError(t, err)

db, err := sql.Open("sqlite3", tmpfile.Name())
assert.NoError(t, err)
defer func() {
_ = db.Close()
}()

sqlStmt := "CREATE TABLE test_table (id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(32));"
_, err = db.Exec(sqlStmt)
assert.NoError(t, err)

tx, err := db.Begin()
assert.NoError(t, err)
func() {
stmt, err := tx.Prepare("INSERT INTO test_table(id, name) values(?, ?)")
assert.NoError(t, err)
defer func() {
_ = stmt.Close()
}()
_, err = stmt.Exec(1, "foo")
assert.NoError(t, err)
}()
func() {
stmt, err := tx.Prepare("INSERT INTO test_table(id) values(?)")
assert.NoError(t, err)
defer func() {
_ = stmt.Close()
}()
_, err = stmt.Exec(2)
assert.NoError(t, err)
}()
err = tx.Commit()
assert.NoError(t, err)

var maybeName Option[string]

row := db.QueryRow("SELECT name FROM test_table WHERE id = 1")
err = row.Scan(&maybeName)
assert.NoError(t, err)
assert.Equal(t, "foo", maybeName.Unwrap())

row = db.QueryRow("SELECT name FROM test_table WHERE id = 2")
err = row.Scan(&maybeName)
assert.NoError(t, err)
assert.True(t, maybeName.IsNone())
}

0 comments on commit 1544d90

Please sign in to comment.