From cddcc494af23efbcd72e62b224418d895c877cfc Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Fri, 27 Feb 2015 03:06:58 +0000 Subject: [PATCH 1/3] Scanner and Valuer for reading and writing time without time zone --- datetime.go | 52 +++++++++++++++++++++++++++ datetime_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 datetime.go create mode 100644 datetime_test.go diff --git a/datetime.go b/datetime.go new file mode 100644 index 000000000..b0ee705f9 --- /dev/null +++ b/datetime.go @@ -0,0 +1,52 @@ +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 +} diff --git a/datetime_test.go b/datetime_test.go new file mode 100644 index 000000000..b8d1a5c74 --- /dev/null +++ b/datetime_test.go @@ -0,0 +1,94 @@ +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) + } + } +} From 3cea327bb8ada3b5c9ad3c8e25117ccb2803f9fc Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Fri, 27 Feb 2015 03:08:58 +0000 Subject: [PATCH 2/3] Scanner and Valuer for reading and writing date --- datetime.go | 67 +++++++++++++++++++++++++++++++++++++++ datetime_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) diff --git a/datetime.go b/datetime.go index b0ee705f9..4bf0b0d66 100644 --- a/datetime.go +++ b/datetime.go @@ -50,3 +50,70 @@ func (c *Clock) scanTime(src time.Time) error { 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 + } +} diff --git a/datetime_test.go b/datetime_test.go index b8d1a5c74..208a554b7 100644 --- a/datetime_test.go +++ b/datetime_test.go @@ -92,3 +92,84 @@ func TestClockValue(t *testing.T) { } } } + +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) + } + } +} From 5ebf8d2f9e8cbd0693077282a49ea2de17fe756d Mon Sep 17 00:00:00 2001 From: Chris Bandy Date: Sun, 1 Mar 2015 21:41:39 +0000 Subject: [PATCH 3/3] Stub types for timestamp and timestamptz --- datetime.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/datetime.go b/datetime.go index 4bf0b0d66..0d5eff82b 100644 --- a/datetime.go +++ b/datetime.go @@ -117,3 +117,22 @@ func (d Date) Value() (driver.Value, error) { 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 +}