diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ca6df..8031c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.4.7] +### Added +- added `Connector` interface (#43) +- added nullable `Time` with better json support (#44) + +### Changed +- renamed `BitBool` with shorter name `Bool` (#44) ## [1.4.6] - 2014-04-23 ### Changed diff --git a/bitbool.go b/bool.go similarity index 71% rename from bitbool.go rename to bool.go index f765b76..28c3b1a 100644 --- a/bitbool.go +++ b/bool.go @@ -5,12 +5,12 @@ import ( "errors" ) -// BitBool is an implementation of a bool for the MySQL type BIT(1). -type BitBool bool +// Bool is an implementation of a bool for the MySQL type BIT(1). +type Bool bool // Value implements the driver.Valuer interface, // and turns the BitBool into a bit field (BIT(1)) for MySQL storage. -func (b BitBool) Value() (driver.Value, error) { // skipcq: GO-W1029 +func (b Bool) Value() (driver.Value, error) { // skipcq: GO-W1029 if b { return []byte{1}, nil } else { @@ -20,7 +20,7 @@ func (b BitBool) Value() (driver.Value, error) { // skipcq: GO-W1029 // Scan implements the sql.Scanner interface, // and turns the bit field incoming from MySQL into a BitBool -func (b *BitBool) Scan(src interface{}) error { // skipcq: GO-W1029 +func (b *Bool) Scan(src interface{}) error { // skipcq: GO-W1029 if src == nil { return nil } diff --git a/bitbool_test.go b/bool_test.go similarity index 91% rename from bitbool_test.go rename to bool_test.go index ed85b97..5c479e6 100644 --- a/bitbool_test.go +++ b/bool_test.go @@ -7,21 +7,21 @@ import ( "github.com/stretchr/testify/require" ) -func TestBitBool(t *testing.T) { +func TestBool(t *testing.T) { d, err := sql.Open("sqlite3", "file::memory:") require.NoError(t, err) _, err = d.Exec("CREATE TABLE `users` (`id` id NOT NULL,`status` BIT(1), PRIMARY KEY (`id`))") require.NoError(t, err) - result, err := d.Exec("INSERT INTO `users`(`id`, `status`) VALUES(?, ?)", 10, BitBool(true)) + result, err := d.Exec("INSERT INTO `users`(`id`, `status`) VALUES(?, ?)", 10, Bool(true)) require.NoError(t, err) rows, err := result.RowsAffected() require.NoError(t, err) require.Equal(t, int64(1), rows) - result, err = d.Exec("INSERT INTO `users`(`id`, `status`) VALUES(?, ?)", 11, BitBool(false)) + result, err = d.Exec("INSERT INTO `users`(`id`, `status`) VALUES(?, ?)", 11, Bool(false)) require.NoError(t, err) rows, err = result.RowsAffected() @@ -42,25 +42,25 @@ func TestBitBool(t *testing.T) { require.NoError(t, err) require.Equal(t, int64(1), rows) - var b1 BitBool + var b1 Bool err = d.QueryRow("SELECT `status` FROM `users` WHERE id=?", 10).Scan(&b1) require.NoError(t, err) require.EqualValues(t, true, b1) - var b2 BitBool + var b2 Bool err = d.QueryRow("SELECT `status` FROM `users` WHERE id=?", 11).Scan(&b2) require.NoError(t, err) require.EqualValues(t, false, b2) - var b3 BitBool + var b3 Bool err = d.QueryRow("SELECT `status` FROM `users` WHERE id=?", 12).Scan(&b3) require.NoError(t, err) require.EqualValues(t, true, b3) - var b4 BitBool + var b4 Bool err = d.QueryRow("SELECT `status` FROM `users` WHERE id=?", 13).Scan(&b4) require.NoError(t, err) diff --git a/time.go b/time.go new file mode 100644 index 0000000..d28b8b7 --- /dev/null +++ b/time.go @@ -0,0 +1,65 @@ +package sqle + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "time" +) + +var nullTimeJsonBytes = []byte("null") + +const nullTimeJson = "null" + +// Time represents a nullable time value. +type Time struct { + sql.NullTime +} + +// NewTime creates a new Time object with the given time and valid flag. +func NewTime(t time.Time, valid bool) Time { + return Time{NullTime: sql.NullTime{Time: t, Valid: valid}} +} + +// Scan implements the [sql.Scanner] interface. +func (t *Time) Scan(value any) error { // skipcq: GO-W1029 + return t.NullTime.Scan(value) +} + +// Value implements the [driver.Valuer] interface. +func (t Time) Value() (driver.Value, error) { // skipcq: GO-W1029 + return t.NullTime.Value() +} + +// Time returns the underlying time.Time value of the Time struct. +func (t *Time) Time() time.Time { // skipcq: GO-W1029 + return t.NullTime.Time +} + +// MarshalJSON implements the json.Marshaler interface +func (t Time) MarshalJSON() ([]byte, error) { // skipcq: GO-W1029 + if t.Valid { + return json.Marshal(t.NullTime.Time) + } + return nullTimeJsonBytes, nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface +func (t *Time) UnmarshalJSON(data []byte) error { // skipcq: GO-W1029 + if len(data) == 0 || string(data) == nullTimeJson { + t.NullTime.Time = time.Time{} + t.NullTime.Valid = false + return nil + } + + var v time.Time + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + + t.NullTime.Time = v + t.NullTime.Valid = true + + return nil +} diff --git a/time_test.go b/time_test.go new file mode 100644 index 0000000..980e36c --- /dev/null +++ b/time_test.go @@ -0,0 +1,113 @@ +package sqle + +import ( + "database/sql" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestTimeInSQL(t *testing.T) { + + now := time.Now() + d, err := sql.Open("sqlite3", "file::memory:") + require.NoError(t, err) + + _, err = d.Exec("CREATE TABLE `times` (`id` id NOT NULL,`created_at` datetime, PRIMARY KEY (`id`))") + require.NoError(t, err) + + result, err := d.Exec("INSERT INTO `times`(`id`) VALUES(?)", 10) + require.NoError(t, err) + + rows, err := result.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), rows) + + result, err = d.Exec("INSERT INTO `times`(`id`, `created_at`) VALUES(?, ?)", 20, now) + require.NoError(t, err) + + rows, err = result.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), rows) + + var t10 Time + err = d.QueryRow("SELECT `created_at` FROM `times` WHERE id=?", 10).Scan(&t10) + require.NoError(t, err) + + require.EqualValues(t, false, t10.Valid) + + var t20 Time + err = d.QueryRow("SELECT `created_at` FROM `times` WHERE id=?", 20).Scan(&t20) + require.NoError(t, err) + + require.EqualValues(t, true, t20.Valid) + require.EqualValues(t, now.UTC(), t20.Time().UTC()) + + result, err = d.Exec("INSERT INTO `times`(`id`,`created_at`) VALUES(?, ?)", 11, t10) + require.NoError(t, err) + + rows, err = result.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), rows) + + result, err = d.Exec("INSERT INTO `times`(`id`, `created_at`) VALUES(?, ?)", 21, t20) + require.NoError(t, err) + + rows, err = result.RowsAffected() + require.NoError(t, err) + require.Equal(t, int64(1), rows) + + var t11 Time + err = d.QueryRow("SELECT `created_at` FROM `times` WHERE id=?", 11).Scan(&t11) + require.NoError(t, err) + + require.EqualValues(t, false, t11.Valid) + + var t21 Time + err = d.QueryRow("SELECT `created_at` FROM `times` WHERE id=?", 21).Scan(&t21) + require.NoError(t, err) + + require.EqualValues(t, true, t21.Valid) + require.EqualValues(t, now.UTC(), t21.Time().UTC()) + +} + +func TestTimeInJSON(t *testing.T) { + + sysTime := time.Now() + + bufSysTime, err := json.Marshal(sysTime) + require.NoError(t, err) + + sqleTime := NewTime(sysTime, true) + + bufSqleTime, err := json.Marshal(sqleTime) + require.NoError(t, err) + + require.Equal(t, bufSysTime, bufSqleTime) + + var jsSqleTime Time + // Unmarshal sqle.Time from time.Time json bytes + err = json.Unmarshal(bufSysTime, &jsSqleTime) + require.NoError(t, err) + + require.True(t, sysTime.Equal(jsSqleTime.Time())) + require.Equal(t, true, jsSqleTime.Valid) + + var jsSysTime time.Time + // Unmarshal time.Time from sqle.Time json bytes + err = json.Unmarshal(bufSqleTime, &jsSysTime) + require.NoError(t, err) + require.True(t, sysTime.Equal(jsSysTime)) + + var nullTime Time + err = json.Unmarshal([]byte("null"), &nullTime) + require.NoError(t, err) + require.Equal(t, false, nullTime.Valid) + + bufNull, err := json.Marshal(nullTime) + require.NoError(t, err) + require.Equal(t, []byte("null"), bufNull) +}