Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions datetime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package pq

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

// Clock represents a value of the PostgreSQL `time without time zone` type.
// It implements the sql.Scanner interface so it can be used as a scan
// destination.
type Clock struct {
Hour, Minute, Second, Nanosecond int
}

// Scan implements the sql.Scanner interface.
func (c *Clock) Scan(src interface{}) error {
switch src := src.(type) {
case []byte:
return c.scanString(string(src))

case string:
return c.scanString(src)

case time.Time:
return c.scanTime(src)
}

return fmt.Errorf("pq: cannot convert %T to Clock", src)
}

func (c *Clock) scanString(src string) (err error) {
t, err := time.Parse("15:04:05", src)
if err == nil {
err = c.scanTime(t)
}

return
}

func (c *Clock) scanTime(src time.Time) error {
hour, min, sec := src.Clock()
nsec := src.Nanosecond()

*c = Clock{Hour: hour, Minute: min, Second: sec, Nanosecond: nsec}
return nil
}

// Value implements the driver.Valuer interface.
func (c Clock) Value() (driver.Value, error) {
return fmt.Sprintf("%02d:%02d:%02d.%09d", c.Hour, c.Minute, c.Second, c.Nanosecond), nil
}

// Date represents a value of the PostgreSQL `date` type. It supports the
// special values "infinity" and "-infinity" and implements the sql.Scanner
// interface so it can be used as a scan destination.
//
// A positive or negative value in Infinity represents the special value
// "infinity" or "-infinity", respectively.
type Date struct {
Infinity int
Year int
Month time.Month
Day int
}

// Scan implements the sql.Scanner interface.
func (d *Date) Scan(src interface{}) error {
switch src := src.(type) {
case []byte:
return d.scanString(string(src))

case string:
return d.scanString(src)

case time.Time:
return d.scanTime(src)
}

return fmt.Errorf("pq: cannot convert %T to Date", src)
}

func (d *Date) scanString(src string) (err error) {
switch src {
case "-infinity":
*d = Date{Infinity: -1}

case "infinity":
*d = Date{Infinity: 1}

default:
t, err := time.Parse("2006-01-02", src)
if err == nil {
err = d.scanTime(t)
}
}

return
}

func (d *Date) scanTime(src time.Time) error {
year, month, day := src.Date()
*d = Date{Year: year, Month: month, Day: day}
return nil
}

// Value implements the driver.Valuer interface.
func (d Date) Value() (driver.Value, error) {
switch {
case d.Infinity < 0:
return "-infinity", nil

case d.Infinity > 0:
return "infinity", nil

default:
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day), nil
}
}

// Timestamp represents a value of the PostgreSQL `timestamp without time zone`
// type. It supports the special values "infinity" and "-infinity" and
// implements the sql.Scanner interface so it can be used as a scan destination.
type Timestamp struct {
Date
Clock
}

// TimestampTZ represents a value of the PostgreSQL `timestamp with time zone`
// type. It supports the special values "infinity" and "-infinity" and
// implements the sql.Scanner interface so it can be used as a scan destination.
//
// A positive or negative value in Infinity represents the special value
// "infinity" or "-infinity", respectively.
type TimestampTZ struct {
Infinity int
Time time.Time
}
175 changes: 175 additions & 0 deletions datetime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package pq

import (
"strings"
"testing"
"time"
)

func TestClockScanUnsupported(t *testing.T) {
var clock Clock
err := clock.Scan(true)

if err == nil {
t.Fatal("Expected error when scanning from bool")
}
if !strings.Contains(err.Error(), "bool to Clock") {
t.Errorf("Expected type to be mentioned when scanning, got %q", err)
}
}

func TestClockScanTime(t *testing.T) {
clock := Clock{9, 9, 9, 9}
err := clock.Scan(time.Date(2001, time.February, 3, 4, 5, 6, 7, time.UTC))

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if clock != (Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7}) {
t.Errorf("Expected 04:05:06.000000007, got %+v", clock)
}
}

func TestClockScanBytes(t *testing.T) {
for _, tt := range []struct {
bytes []byte
clock Clock
}{
{[]byte(`04:05:06`), Clock{Hour: 4, Minute: 5, Second: 6}},
{[]byte(`04:05:06.007`), Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000000}},
{[]byte(`04:05:06.000007`), Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000}},
} {
clock := Clock{9, 9, 9, 9}
err := clock.Scan(tt.bytes)

if err != nil {
t.Fatalf("Expected no error for %q, got %v", tt.bytes, err)
}
if clock != tt.clock {
t.Errorf("Expected %+v, got %+v", tt.clock, clock)
}
}
}

func TestClockScanString(t *testing.T) {
for _, tt := range []struct {
str string
clock Clock
}{
{`04:05:06`, Clock{Hour: 4, Minute: 5, Second: 6}},
{`04:05:06.007`, Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000000}},
{`04:05:06.000007`, Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000}},
} {
clock := Clock{9, 9, 9, 9}
err := clock.Scan(tt.str)

if err != nil {
t.Fatalf("Expected no error for %q, got %v", tt.str, err)
}
if clock != tt.clock {
t.Errorf("Expected %+v, got %+v", tt.clock, clock)
}
}
}

func TestClockValue(t *testing.T) {
for _, tt := range []struct {
str string
clock Clock
}{
{`04:05:06.000000000`, Clock{Hour: 4, Minute: 5, Second: 6}},
{`04:05:06.007000000`, Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000000}},
{`04:05:06.000007000`, Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7000}},
{`04:05:06.000000007`, Clock{Hour: 4, Minute: 5, Second: 6, Nanosecond: 7}},
} {
value, err := tt.clock.Value()

if err != nil {
t.Fatalf("Expected no error for %q, got %v", tt.clock, err)
}
if value != tt.str {
t.Errorf("Expected %v, got %v", tt.str, value)
}
}
}

func TestDateScanUnsupported(t *testing.T) {
var date Date
err := date.Scan(true)

if err == nil {
t.Fatal("Expected error when scanning from bool")
}
if !strings.Contains(err.Error(), "bool to Date") {
t.Errorf("Expected type to be mentioned when scanning, got %q", err)
}
}

func TestDateScanTime(t *testing.T) {
date := Date{9, 9, 9, 9}
err := date.Scan(time.Date(2001, time.February, 3, 4, 5, 6, 7, time.UTC))

if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if date != (Date{Year: 2001, Month: 2, Day: 3}) {
t.Errorf("Expected 2001-02-03, got %v", date)
}
}

func TestDateScanBytes(t *testing.T) {
for _, tt := range []struct {
bytes []byte
date Date
}{
{[]byte(`infinity`), Date{Infinity: 1}},
{[]byte(`-infinity`), Date{Infinity: -1}},
{[]byte(`2001-02-03`), Date{Year: 2001, Month: 2, Day: 3}},
} {
date := Date{9, 9, 9, 9}
err := date.Scan(tt.bytes)

if err != nil {
t.Fatalf("Expected no error for %q, got %v", tt.bytes, err)
}
if date != tt.date {
t.Errorf("Expected %+v, got %+v", tt.date, date)
}
}
}

var DateStringTests = []struct {
str string
date Date
}{
{`infinity`, Date{Infinity: 1}},
{`-infinity`, Date{Infinity: -1}},
{`2001-02-03`, Date{Year: 2001, Month: 2, Day: 3}},
}

func TestDateScanString(t *testing.T) {
for _, tt := range DateStringTests {
date := Date{9, 9, 9, 9}
err := date.Scan(tt.str)

if err != nil {
t.Fatalf("Expected no error for %q, got %v", tt.str, err)
}
if date != tt.date {
t.Errorf("Expected %+v, got %+v", tt.date, date)
}
}
}

func TestDateValue(t *testing.T) {
for _, tt := range DateStringTests {
value, err := tt.date.Value()

if err != nil {
t.Fatalf("Expected no error for %q, got %v", tt.date, err)
}
if value != tt.str {
t.Errorf("Expected %v, got %v", tt.str, value)
}
}
}