Skip to content

Conversation

@cbandy
Copy link
Contributor

@cbandy cbandy commented Mar 1, 2015

The features provided by the temporal types of Postgres differ from those of Go's time.Time. We should read and write values of these types in such a way as to:

  • Preserve their meaning
  • Minimize the physical effort (user code) required to use them
  • Minimize the mental effort (cognitive load) required to use them
  • Maximize the ways in which they can be used
  • Minimize the burden of maintaining this package
  • Maximize performance

This PR proposes to use a handful of Go types that implement sql.Scanner and driver.Valuer to achieve these goals.

Preserve meaning

The date, timestamp and timestamptz types support infinite values which are explicitly outside the range of non-infinite (i.e. normal) values. The proposed pq.Date, pq.Timestamp and pq.TimestampTZ types have an Infinity field to represent these special values.

The Postgres temporal types that lack a date or a time zone contain only as much information as a calendar or a wall clock. The proposed types contain the same amount of information (e.g. no default time zone.)

Maximize usability

When not concerned about infinite values, a user can scan a timestamptz value directly into a time.Time.

Multiple proposed types can be used as scan destinations for multiple Postgres temporal values. For example, a timestamp can be scanned into a pq.Date.

When using pq.Clock, pq.Date or pq.Timestamp, a user need not consider the behavior of the time package at all. Even so, these types are congruous with the time package. For example, one may implement an sql.Scanner that uses these types as intermediaries, passing their fields directly to time.Date().

Minimize maintenance

Since there is little to no guidance for how a driver should return time.Time values, it is important to clearly document how temporal values are handled. The proposed, exported types provide a natural place for such documentation, including the disparities between Postgres temporal types and the time package.

The composition of these types should result in a few small units of shared code, and these units will likely return error to match the sql.Scanner interface. The related tests can be focused, succinct and thorough.

Maximize performance

For most temporal types, parsing can be deferred until the scan destination is known. In these cases, it is possible to limit processing to only the fields requested by the user. For example, only the year, month and day of a timestamp value need to be parsed when populating a pq.Date.

For these same types, the driver is responsible only for parsing the various Postgres output formats into numeric fields. The user decides if/when to incur the cost of calculating a time.Time value.

Mappings to/from Go and Postgres

From Backend
Data Type Value Compatible Scanners
timestamp []byte pq.Date, pq.Timestamp
timestamptz []byte,1 time.Time pq.Date, pq.Timestamp, pq.TimestampTZ
date []byte pq.Date
time []byte pq.Clock
timetz []byte ?
To Backend
Valuer Value Compatible Data Types
n/a time.Time date,2 time,2 timetz, timestamp,2 timestamptz
pq.Timestamp []byte date, timestamp, timestamptz3
pq.TimestampTZ []byte date,2 timestamp,2 timestamptz
pq.Date []byte date, timestamp, timestamptz3
pq.Clock []byte time, timetz3

1 When "infinity" or "-infinity"
2 Time zone is silently ignored
3 Interpreted as being in the session time zone

Transition

The proposed types work correctly without any changes to the existing driver encode/decode process. The existing (driver) parsing and formatting code can be refactored toward this interface, eliminating duplicated functionality.

The (expected) performance gains from returning []byte for most types can be activated with a package-level configuration.

If this interface is preferred by maintainers, the existing/default behavior can be deprecated and eventually eliminated.

@cbandy
Copy link
Contributor Author

cbandy commented Mar 1, 2015

I used time.Parse and fmt.Sprintf as quick examples. A complete implementation would handle "BC" dates, etc.

@johto
Copy link
Contributor

johto commented Mar 16, 2015

I've looked at this again, and I'm liking a lot of what I'm seeing. However, I'm not sure that only getting the values out of the database is going to be enough. Have you considered at all whether any additional functionality should be provided, and if so, what kind? For an example, the first thing that comes to mind is how difficult (AFAIK, at least) it is to currently do the equivalent of "timestamp AT TIME ZONE .." in Go when the timestamp (without time zone) comes from the database.

The (expected) performance gains from returning []byte for most types can be activated with a package-level configuration.

I'm confused about what you mean by this. Can you elaborate on this point a bit?

@cbandy
Copy link
Contributor Author

cbandy commented Mar 17, 2015

Have you considered at all whether any additional functionality should be provided, and if so, what kind?

In my experience, any kind of arithmetic should happen in the full context of a particular location/locale. Go's time.Time encourages users to do the same.

I thought about providing methods that make it a little easier for users to convert these values to time.Time:

// Time builds a time.Time object representing the value of this clock
// at a specific date and location.
func (c Clock) Time(year int, month time.Month, day int, loc *time.Location) time.Time {
  return time.Date(year, month, day, c.Hour, c.Minute, c.Second, c.Nanosecond, loc)
}

But we would have to decide how to handle infinite values for date and timestamp types.

For an example, the first thing that comes to mind is how difficult (AFAIK, at least) it is to currently do the equivalent of "timestamp AT TIME ZONE .." in Go when the timestamp (without time zone) comes from the database.

"Currently" with this proposal or without? With this proposal, one need only pass the fields of pq.Timestamp to time.Date() along with the desired time.Location (similar to time.ParseInLocation().) Here is a Scanner that sets the Location to UTC:

type TimeInMyAppLocation time.Time

func (t *TimeInMyAppLocation) Scan(src interface{}) (err error) {
  var ts pq.Timestamp
  if err = ts.Scan(src); err == nil {
    *t = TimeInMyAppLocation(time.Date(
        ts.Year, ts.Month, ts.Day,
        ts.Hour, ts.Minute, ts.Second, ts.Nanosecond,
        time.UTC))
  }
  return
}

The (expected) performance gains from returning []byte for most types can be activated with a package-level configuration.

I'm confused about what you mean by this. Can you elaborate on this point a bit?

I mean that we can allow the user to choose, through some configuration, whether temporal values are returned as time.Time (existing behavior) or []byte (proposed behavior.) I hypothesized two scenarios in which the proposed behavior might outperform the existing behavior.

@cbandy
Copy link
Contributor Author

cbandy commented Sep 21, 2015

#391 indicates that not all canonical representations of time.Time are acceptable for date, time, and timetz types. If so, some tables need to be updated here.

@calebhearth
Copy link
Contributor

I agree that a convenience method to extract the time would be useful. The current behavior when scanning a time column into time.Time sets what I consider to be reasonable defaults: time.Date(0, 1, 1, <hour>, <minute>, <second>, <nanosecond>, time.UTC), so something like that seems useful. Combined with the proposed Time func:

// Time builds a time.Time object representing the value of this clock
// at a specific date and location.
func (c Clock) Time(year int, month time.Month, day int, loc *time.Location) time.Time {
  return time.Date(year, month, day, c.Hour, c.Minute, c.Second, c.Nanosecond, loc)
}

And a DefaultTime could be implemented as c.Time(0, 1, 1, time.UTC).

@calebhearth
Copy link
Contributor

That also means we can pretty easily do AT TIME ZONE by converting to time.Time and using time.Time.In in consumer apps.

Or we could write a convenience function:

func (c Clock) AtTimeZone(loc time.Location) (ret Clock) {
  ret.Scan(c.DefaultTime.In(loc))
  return
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants