github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/timeutil/pgdate/parsing_test.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package pgdate_test
    12  
    13  import (
    14  	gosql "database/sql"
    15  	"flag"
    16  	"fmt"
    17  	"os"
    18  	"strings"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/cockroachdb/cockroach/pkg/util/timeutil/pgdate"
    23  	_ "github.com/lib/pq"
    24  )
    25  
    26  var modes = []pgdate.ParseMode{
    27  	pgdate.ParseModeDMY,
    28  	pgdate.ParseModeMDY,
    29  	pgdate.ParseModeYMD,
    30  }
    31  
    32  var db *gosql.DB
    33  var dbString string
    34  
    35  func init() {
    36  	flag.StringVar(&dbString, "pgdate.db", "",
    37  		`a postgresql connect string suitable for sql.Open(), `+
    38  			`to enable cross-checking during development; for example: `+
    39  			`-pgdate.db="database=bob sslmode=disable"`)
    40  }
    41  
    42  type timeData struct {
    43  	s string
    44  	// The generally-expected value.
    45  	exp time.Time
    46  	// Is an error expected?
    47  	err bool
    48  	// Allow leniency when comparing cross-checked values.
    49  	allowCrossDelta time.Duration
    50  	// Disable cross-checking for unimplemented features.
    51  	expectCrossErr bool
    52  	// Special-case for some weird times that roll over to the next day.
    53  	isRolloverTime bool
    54  	// This value isn't expected to be successful if concatenated.
    55  	expectConcatErr bool
    56  	// This text contains a timezone, so we wouldn't expect to be
    57  	// able to combine it with another timezone-containing value.
    58  	hasTimezone bool
    59  	// Override the expected value for a given ParseMode.
    60  	modeExp map[pgdate.ParseMode]time.Time
    61  	// Override the expected error for a given ParseMode.
    62  	modeErr map[pgdate.ParseMode]bool
    63  	// Indicates that we don't implement a feature in PostgreSQL.
    64  	unimplemented bool
    65  }
    66  
    67  // concatTime creates a derived timeData that represents date data
    68  // concatenated with time data to produce timestamp data.
    69  func (td timeData) concatTime(other timeData) timeData {
    70  	add := func(d time.Time, t time.Time) time.Time {
    71  		year, month, day := d.Date()
    72  		hour, min, sec := t.Clock()
    73  
    74  		// Prefer whichever has a non-UTC location. You're guaranteed
    75  		// to get an error anyway if you concatenate TZ-containing strings.
    76  		loc := d.Location()
    77  		if loc == time.UTC {
    78  			loc = t.Location()
    79  		}
    80  
    81  		return time.Date(year, month, day, hour, min, sec, t.Nanosecond(), loc)
    82  	}
    83  
    84  	concatErr := other.err || td.expectConcatErr || other.expectConcatErr || (td.hasTimezone && other.hasTimezone)
    85  
    86  	var concatModeExp map[pgdate.ParseMode]time.Time
    87  	if td.modeExp != nil && !concatErr {
    88  		concatModeExp = make(map[pgdate.ParseMode]time.Time, len(td.modeExp))
    89  		for mode, date := range td.modeExp {
    90  			concatModeExp[mode] = add(date, other.exp)
    91  		}
    92  	}
    93  
    94  	delta := td.allowCrossDelta
    95  	if other.allowCrossDelta > delta {
    96  		delta = other.allowCrossDelta
    97  	}
    98  
    99  	return timeData{
   100  		s:               fmt.Sprintf("%s %s", td.s, other.s),
   101  		exp:             add(td.exp, other.exp),
   102  		err:             td.err || concatErr,
   103  		allowCrossDelta: delta,
   104  		expectCrossErr:  td.expectCrossErr || other.expectCrossErr,
   105  		hasTimezone:     td.hasTimezone || other.hasTimezone,
   106  		isRolloverTime:  td.isRolloverTime || other.isRolloverTime,
   107  		modeExp:         concatModeExp,
   108  		modeErr:         td.modeErr,
   109  		unimplemented:   td.unimplemented || other.unimplemented,
   110  	}
   111  }
   112  
   113  // expected returns the expected time or expected error condition for the mode.
   114  func (td timeData) expected(mode pgdate.ParseMode) (time.Time, bool) {
   115  	if t, ok := td.modeExp[mode]; ok {
   116  		return t, false
   117  	}
   118  	if _, ok := td.modeErr[mode]; ok {
   119  		return pgdate.TimeEpoch, true
   120  	}
   121  	return td.exp, td.err
   122  }
   123  
   124  func (td timeData) testParseDate(t *testing.T, info string, mode pgdate.ParseMode) {
   125  	info = fmt.Sprintf("%s ParseDate", info)
   126  	exp, expErr := td.expected(mode)
   127  	dt, err := pgdate.ParseDate(time.Time{}, mode, td.s)
   128  	res, _ := dt.ToTime()
   129  
   130  	// HACK: This is a format that parses as a date and timestamp,
   131  	// but is not a time.
   132  	if td.s == "2018 123" {
   133  		exp = time.Date(2018, 5, 3, 0, 0, 0, 0, time.UTC)
   134  		expErr = false
   135  	}
   136  
   137  	// Keeps the date components, but lose everything else.
   138  	y, m, d := exp.Date()
   139  	exp = time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
   140  
   141  	check(t, info, exp, expErr, res, err)
   142  
   143  	td.crossCheck(t, info, "date", td.s, mode, exp, expErr)
   144  }
   145  
   146  func (td timeData) testParseTime(t *testing.T, info string, mode pgdate.ParseMode) {
   147  	info = fmt.Sprintf("%s ParseTime", info)
   148  	exp, expErr := td.expected(mode)
   149  	res, err := pgdate.ParseTime(time.Time{}, mode, td.s)
   150  
   151  	// Weird times like 24:00:00 or 23:59:60 aren't allowed,
   152  	// unless there's also a date.
   153  	if td.isRolloverTime {
   154  		_, err := pgdate.ParseDate(time.Time{}, mode, td.s)
   155  		expErr = err != nil
   156  	}
   157  
   158  	// Keep only the time and zone components.
   159  	h, m, sec := exp.Clock()
   160  	exp = time.Date(0, 1, 1, h, m, sec, td.exp.Nanosecond(), td.exp.Location())
   161  
   162  	check(t, info, exp, expErr, res, err)
   163  	td.crossCheck(t, info, "timetz", td.s, mode, exp, expErr)
   164  }
   165  
   166  func (td timeData) testParseTimestamp(t *testing.T, info string, mode pgdate.ParseMode) {
   167  	info = fmt.Sprintf("%s ParseTimestamp", info)
   168  	exp, expErr := td.expected(mode)
   169  	res, err := pgdate.ParseTimestamp(time.Time{}, mode, td.s)
   170  
   171  	// HACK: This is a format that parses as a date and timestamp,
   172  	// but is not a time.
   173  	if td.s == "2018 123" {
   174  		exp = time.Date(2018, 5, 3, 0, 0, 0, 0, time.UTC)
   175  		expErr = false
   176  	}
   177  
   178  	if td.isRolloverTime {
   179  		exp = exp.AddDate(0, 0, 1)
   180  	}
   181  
   182  	check(t, info, exp, expErr, res, err)
   183  	td.crossCheck(t, info, "timestamptz", td.s, mode, exp, expErr)
   184  }
   185  
   186  var dateTestData = []timeData{
   187  	// The cases below are taken from
   188  	// https://github.com/postgres/postgres/blob/REL_10_5/src/test/regress/sql/date.sql
   189  	// and with comments from
   190  	// https://www.postgresql.org/docs/10/static/datatype-datetime.html#DATATYPE-DATETIME-DATE-TABLE
   191  	{
   192  		//January 8, 1999	unambiguous in any datestyle input mode
   193  		s:   "January 8, 1999",
   194  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   195  	},
   196  	{
   197  		//1999-01-08	ISO 8601; January 8 in any mode (recommended format)
   198  		s:   "1999-01-08",
   199  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   200  	},
   201  	{
   202  		//1999-01-18	ISO 8601; January 18 in any mode (recommended format)
   203  		s:   "1999-01-18",
   204  		exp: time.Date(1999, time.January, 18, 0, 0, 0, 0, time.UTC),
   205  	},
   206  	{
   207  		//1/8/1999	January 8 in MDY mode; August 1 in DMY mode
   208  		s:   "1/8/1999",
   209  		err: true,
   210  		modeExp: map[pgdate.ParseMode]time.Time{
   211  			pgdate.ParseModeDMY: time.Date(1999, time.August, 1, 0, 0, 0, 0, time.UTC),
   212  			pgdate.ParseModeMDY: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   213  		},
   214  	},
   215  	{
   216  		// 1/18/1999 January 18 in MDY mode; rejected in other modes
   217  		s:   "1/18/1999",
   218  		err: true,
   219  		modeExp: map[pgdate.ParseMode]time.Time{
   220  			pgdate.ParseModeMDY: time.Date(1999, time.January, 18, 0, 0, 0, 0, time.UTC),
   221  		},
   222  	},
   223  	{
   224  		// 18/1/1999 January 18 in DMY mode; rejected in other modes
   225  		s:   "18/1/1999",
   226  		err: true,
   227  		modeExp: map[pgdate.ParseMode]time.Time{
   228  			pgdate.ParseModeDMY: time.Date(1999, time.January, 18, 0, 0, 0, 0, time.UTC),
   229  		},
   230  	},
   231  	{
   232  		// 01/02/03	January 2, 2003 in MDY mode; February 1, 2003 in DMY mode; February 3, 2001 in YMD mode
   233  		s: "01/02/03",
   234  		modeExp: map[pgdate.ParseMode]time.Time{
   235  			pgdate.ParseModeYMD: time.Date(2001, time.February, 3, 0, 0, 0, 0, time.UTC),
   236  			pgdate.ParseModeDMY: time.Date(2003, time.February, 1, 0, 0, 0, 0, time.UTC),
   237  			pgdate.ParseModeMDY: time.Date(2003, time.January, 2, 0, 0, 0, 0, time.UTC),
   238  		},
   239  	},
   240  	{
   241  		// 19990108	ISO 8601; January 8, 1999 in any mode
   242  		s:   "19990108",
   243  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   244  	},
   245  	{
   246  		// 990108	ISO 8601; January 8, 1999 in any mode
   247  		s:   "990108",
   248  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   249  	},
   250  	{
   251  		// 1999.008	year and day of year
   252  		s:   "1999.008",
   253  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   254  	},
   255  	{
   256  		// J2451187	Julian date
   257  		s:   "J2451187",
   258  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   259  	},
   260  	{
   261  		// January 8, 99 BC	year 99 BC
   262  		s: "January 8, 99 BC",
   263  		// Note that this is off by one
   264  		exp: time.Date(-98, time.January, 8, 0, 0, 0, 0, time.UTC),
   265  		// Failure confirmed in pg 10.5:
   266  		// https://github.com/postgres/postgres/blob/REL_10_5/src/test/regress/expected/date.out#L135
   267  		modeErr: map[pgdate.ParseMode]bool{
   268  			pgdate.ParseModeYMD: true,
   269  		},
   270  	},
   271  
   272  	{
   273  		// 99-Jan-08	January 8 in YMD mode, else error
   274  		s:   "99-Jan-08",
   275  		err: true,
   276  		modeExp: map[pgdate.ParseMode]time.Time{
   277  			pgdate.ParseModeYMD: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   278  		},
   279  	},
   280  	{
   281  		// 1999-Jan-08	January 8 in any mode
   282  		s:   "1999-Jan-08",
   283  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   284  	},
   285  	{
   286  		// 08-Jan-99	January 8, except error in YMD mode
   287  		s:   "08-Jan-99",
   288  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   289  		modeErr: map[pgdate.ParseMode]bool{
   290  			pgdate.ParseModeYMD: true,
   291  		},
   292  	},
   293  	{
   294  		// 08-Jan-1999	January 8 in any mode
   295  		s:   "08-Jan-1999",
   296  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   297  	},
   298  	{
   299  		// Jan-08-99	January 8, except error in YMD mode
   300  		s:   "Jan-08-99",
   301  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   302  		modeErr: map[pgdate.ParseMode]bool{
   303  			pgdate.ParseModeYMD: true,
   304  		},
   305  	},
   306  	{
   307  		// Jan-08-1999	January 8 in any mode
   308  		s:   "Jan-08-1999",
   309  		exp: time.Date(1999, time.January, 8, 0, 0, 0, 0, time.UTC),
   310  	},
   311  	{
   312  		// 99-08-Jan Error in all modes, because 99 isn't obviously a year
   313  		// and there's no YDM parse mode.
   314  		s:   "99-08-Jan",
   315  		err: true,
   316  	},
   317  	{
   318  		// 1999-08-Jan, for consistency with test above.
   319  		s:   "1999-08-Jan",
   320  		err: true,
   321  	},
   322  
   323  	// ------- More tests ---------
   324  	{
   325  		// Two sentinels
   326  		s:   "epoch infinity",
   327  		err: true,
   328  	},
   329  	{
   330  		// Provide too few fields
   331  		s:   "2018",
   332  		err: true,
   333  	},
   334  	{
   335  		// Provide too few fields
   336  		s:   "2018-10",
   337  		err: true,
   338  	},
   339  	{
   340  		// Provide a full timestamp.
   341  		s:               "2017-12-05 04:04:04.913231+00:00",
   342  		exp:             time.Date(2017, time.December, 05, 0, 0, 0, 0, time.UTC),
   343  		expectConcatErr: true,
   344  		hasTimezone:     true,
   345  	},
   346  	{
   347  		// Date from a full nano-time.
   348  		s:               "2006-07-08T00:00:00.000000123Z",
   349  		exp:             time.Date(2006, time.July, 8, 0, 0, 0, 0, time.UTC),
   350  		expectConcatErr: true,
   351  		hasTimezone:     true,
   352  	},
   353  	{
   354  		s:   "Random input",
   355  		err: true,
   356  	},
   357  	{
   358  		// Random date with a timezone.
   359  		s:           "2018-10-23 +01",
   360  		exp:         time.Date(2018, 10, 23, 0, 0, 0, 0, time.FixedZone("", 60*60)),
   361  		hasTimezone: true,
   362  	},
   363  	{
   364  		s:   "5874897-01-22",
   365  		exp: time.Date(5874897, 1, 22, 0, 0, 0, 0, time.UTC),
   366  	},
   367  	{
   368  		s:   "121212-01-01",
   369  		exp: time.Date(121212, 1, 1, 0, 0, 0, 0, time.UTC),
   370  	},
   371  	{
   372  		s:   "121212",
   373  		exp: time.Date(2012, 12, 12, 0, 0, 0, 0, time.UTC),
   374  	},
   375  }
   376  
   377  var timeTestData = []timeData{
   378  	{
   379  		// 04:05:06.789 ISO 8601
   380  		s:   "04:05:06.789",
   381  		exp: time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   382  	},
   383  	{
   384  		//  04:05:06 ISO 8601
   385  		s:   "04:05:06",
   386  		exp: time.Date(0, 1, 1, 4, 5, 6, 0, time.UTC),
   387  	},
   388  	{
   389  		//  04:05 ISO 8601
   390  		s:   "04:05",
   391  		exp: time.Date(0, 1, 1, 4, 5, 0, 0, time.UTC),
   392  	},
   393  	{
   394  		//  040506 ISO 8601
   395  		s:   "040506",
   396  		exp: time.Date(0, 1, 1, 4, 5, 6, 0, time.UTC),
   397  	},
   398  	{
   399  		//  04:05 AM same as 04:05; AM does not affect value
   400  		s:   "04:05 AM",
   401  		exp: time.Date(0, 1, 1, 4, 5, 0, 0, time.UTC),
   402  	},
   403  	{
   404  		//  04:05 PM same as 16:05; input hour must be <= 12
   405  		s:   "04:05 PM",
   406  		exp: time.Date(0, 1, 1, 16, 5, 0, 0, time.UTC),
   407  	},
   408  	{
   409  		// 04:05:06.789-8 ISO 8601
   410  		s:           "04:05:06.789-8",
   411  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.FixedZone("-0800", -8*60*60)),
   412  		hasTimezone: true,
   413  	},
   414  	{
   415  		// 04:05:06.789-8:30 ISO 8601
   416  		s:           "04:05:06.789-8:30",
   417  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.FixedZone("-0830", -8*60*60-30*60)),
   418  		hasTimezone: true,
   419  	},
   420  	{
   421  		// 04:05-8:00 ISO 8601
   422  		s:           "04:05-8:00",
   423  		exp:         time.Date(0, 1, 1, 4, 5, 0, 0, time.FixedZone("-0800", -8*60*60)),
   424  		hasTimezone: true,
   425  	},
   426  	{
   427  		// 040506-08 ISO 8601
   428  		s:           "040506-8",
   429  		exp:         time.Date(0, 1, 1, 4, 5, 6, 0, time.FixedZone("-0800", -8*60*60)),
   430  		hasTimezone: true,
   431  	},
   432  	{
   433  		// 04:05:06 PST time zone specified by abbreviation
   434  		// Unimplemented with message to user as such:
   435  		// https://github.com/cockroachdb/cockroach/issues/31710
   436  		s:   "04:05:06 PST",
   437  		err: true,
   438  		// This should be the value if/when we implement this.
   439  		exp:           time.Date(0, 1, 1, 4, 5, 6, 0, time.FixedZone("-0800", -8*60*60)),
   440  		hasTimezone:   true,
   441  		unimplemented: true,
   442  	},
   443  	{
   444  		// This test, and the next show that resolution of geographic names
   445  		// to actual timezones is aware of daylight-savings time.  Note
   446  		// that even though we're just parsing a time value, we do need
   447  		// to provide a date in order to resolve the named zone to a
   448  		// UTC offset.
   449  		s:               "2003-01-12 04:05:06 America/New_York",
   450  		exp:             time.Date(0, 1, 1, 4, 5, 6, 0, time.FixedZone("-0500", -5*60*60)),
   451  		expectConcatErr: true,
   452  		hasTimezone:     true,
   453  	},
   454  	{
   455  		s:               "2003-06-12 04:05:06 America/New_York",
   456  		exp:             time.Date(0, 1, 1, 4, 5, 6, 0, time.FixedZone("-0400", -4*60*60)),
   457  		expectConcatErr: true,
   458  		hasTimezone:     true,
   459  	},
   460  
   461  	// ----- More Tests -----
   462  	{
   463  		// Check positive TZ offsets.
   464  		s:           "04:05:06.789+8:30",
   465  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.FixedZone("", 8*60*60+30*60)),
   466  		hasTimezone: true,
   467  	},
   468  	{
   469  		// Check TZ with seconds.
   470  		s:           "04:05:06.789+8:30:15",
   471  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.FixedZone("", 8*60*60+30*60+15)),
   472  		hasTimezone: true,
   473  	},
   474  	{
   475  		// Check packed TZ with seconds.
   476  		s:           "04:05:06.789+083015",
   477  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.FixedZone("", 8*60*60+30*60+15)),
   478  		hasTimezone: true,
   479  	},
   480  	{
   481  		// Check UTC zone.
   482  		s:           "04:05:06.789 UTC",
   483  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   484  		hasTimezone: true,
   485  	},
   486  	{
   487  		// Check GMT zone.
   488  		s:           "04:05:06.789 GMT",
   489  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   490  		hasTimezone: true,
   491  	},
   492  	{
   493  		// Check Z suffix with space.
   494  		s:           "04:05:06.789 z",
   495  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   496  		hasTimezone: true,
   497  	},
   498  	{
   499  		// Check Zulu suffix with space.
   500  		s:           "04:05:06.789 zulu",
   501  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   502  		hasTimezone: true,
   503  	},
   504  	{
   505  		// Check Z suffix without space.
   506  		s:           "04:05:06.789z",
   507  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   508  		hasTimezone: true,
   509  	},
   510  	{
   511  		// Check Zulu suffix without space.
   512  		s:           "04:05:06.789zulu",
   513  		exp:         time.Date(0, 1, 1, 4, 5, 6, int(789*time.Millisecond), time.UTC),
   514  		hasTimezone: true,
   515  	},
   516  	{
   517  		// Packed time should extra seconds.
   518  		s:   "045:06",
   519  		err: true,
   520  	},
   521  	{
   522  		// Check 12:54 AM -> 0
   523  		s:   "12:54 AM",
   524  		exp: time.Date(0, 1, 1, 0, 54, 0, 0, time.UTC),
   525  	},
   526  	{
   527  		// Check 12:54 PM -> 12
   528  		s:   "12:54 PM",
   529  		exp: time.Date(0, 1, 1, 12, 54, 0, 0, time.UTC),
   530  	},
   531  	{
   532  		// Check 00:54 AM -> 0
   533  		// This behavior is observed in pgsql 10.5.
   534  		s:   "00:54 AM",
   535  		exp: time.Date(0, 1, 1, 0, 54, 0, 0, time.UTC),
   536  	},
   537  	{
   538  		// Check 00:54 PM -> 12
   539  		// This behavior is observed in pgsql 10.5.
   540  		s:   "0:54 PM",
   541  		exp: time.Date(0, 1, 1, 12, 54, 0, 0, time.UTC),
   542  	},
   543  	{
   544  		// Check nonsensical TZ.
   545  		// This behavior is observed in pgsql 10.5.
   546  		s:           "12:54-00:29",
   547  		exp:         time.Date(0, 1, 1, 12, 54, 0, 0, time.FixedZone("UGH", -29*60)),
   548  		hasTimezone: true,
   549  	},
   550  	{
   551  		// Check long timezone with date month.
   552  		s:               "June 12, 2003 04:05:06 America/New_York",
   553  		exp:             time.Date(0, 1, 1, 4, 5, 6, 0, time.FixedZone("-0400", -4*60*60)),
   554  		expectConcatErr: true,
   555  	},
   556  	{
   557  		// Require that minutes and seconds must either be packed or have colon separators.
   558  		s:   "01 02 03",
   559  		err: true,
   560  	},
   561  	{
   562  		// 3-digit times should not work.
   563  		s:   "123",
   564  		err: true,
   565  	},
   566  	{
   567  		//  Single-digits
   568  		s:   "4:5:6",
   569  		exp: time.Date(0, 1, 1, 4, 5, 6, 0, time.UTC),
   570  	},
   571  	{
   572  		// Maximum value
   573  		s: "24:00:00",
   574  		// Allow hour 24 to roll over when we have a date.
   575  		isRolloverTime: true,
   576  	},
   577  	{
   578  		// Exceed maximum value
   579  		s:   "24:00:00.000001",
   580  		err: true,
   581  	},
   582  	{
   583  		s: "23:59:60",
   584  		// Allow this to roll over when we have a date.
   585  		isRolloverTime: true,
   586  	},
   587  	{
   588  		// Even though 24 and 60 are valid hours and seconds, 60 minutes is not.
   589  		s:   "23:60:00",
   590  		err: true,
   591  	},
   592  	{
   593  		// Verify that we do support full nanosecond resolution in parsing.
   594  		s:   "04:05:06.999999999",
   595  		exp: time.Date(0, 1, 1, 4, 5, 6, 999999999, time.UTC),
   596  		// PostgreSQL rounds to the nearest micro,
   597  		// but we have other internal consumers that require nano precision.
   598  		allowCrossDelta: time.Microsecond,
   599  	},
   600  	{
   601  		// Over-long fractional portion gets truncated.
   602  		s:   "04:05:06.9999999999",
   603  		exp: time.Date(0, 1, 1, 4, 5, 6, 999999999, time.UTC),
   604  		// PostgreSQL rounds to the nearest micro,
   605  		// but we have other internal consumers that require nano precision.
   606  		allowCrossDelta: time.Microsecond,
   607  	},
   608  	{
   609  		// Verify that micros are maintained.
   610  		s:   "23:59:59.999999",
   611  		exp: time.Date(0, 1, 1, 23, 59, 59, 999999000, time.UTC),
   612  	},
   613  	{
   614  		// Verify that tenths are maintained.
   615  		s:   "23:59:59.1",
   616  		exp: time.Date(0, 1, 1, 23, 59, 59, 100000000, time.UTC),
   617  	},
   618  }
   619  
   620  // Additional timestamp tests not generated by combining dates and times.
   621  var timestampTestData = []timeData{
   622  	{
   623  		s:   "2000-01-01T02:02:02",
   624  		exp: time.Date(2000, 1, 1, 2, 2, 2, 0, time.UTC),
   625  	},
   626  	{
   627  		s:   "2000-01-01T02:02:02.567",
   628  		exp: time.Date(2000, 1, 1, 2, 2, 2, 567000000, time.UTC),
   629  	},
   630  	{
   631  		s:           "2000-01-01T02:02:02.567+09:30:15",
   632  		exp:         time.Date(2000, 1, 1, 2, 2, 2, 567000000, time.FixedZone("", 9*60*60+30*60+15)),
   633  		hasTimezone: true,
   634  	},
   635  }
   636  
   637  // TestMain will enable cross-checking of test results against a
   638  // PostgreSQL instance if the -pgdate.db flag is set. This is mainly
   639  // useful for developing the tests themselves and doesn't need
   640  // to be part of a regular build.
   641  func TestMain(m *testing.M) {
   642  	if dbString != "" {
   643  		if d, err := gosql.Open("postgres", dbString); err == nil {
   644  			if err := d.Ping(); err == nil {
   645  				db = d
   646  			} else {
   647  				println("could not ping database", err)
   648  				os.Exit(-1)
   649  			}
   650  		} else {
   651  			println("could not open database", err)
   652  			os.Exit(-1)
   653  		}
   654  	}
   655  	os.Exit(m.Run())
   656  }
   657  
   658  // TestParse does the following:
   659  // * For each parsing mode:
   660  //   * Pick an example date input: 2018-01-01
   661  //   * Test ParseDate()
   662  //   * Pick an example time input: 12:34:56
   663  //     * Derive a timestamp from date + time
   664  //     * Test ParseTimestame()
   665  //     * Test ParseDate()
   666  //     * Test ParseTime()
   667  //   * Test one-off timestamp formats
   668  // * Pick an example time input:
   669  //   * Test ParseTime()
   670  func TestParse(t *testing.T) {
   671  	for _, mode := range modes {
   672  		t.Run(mode.String(), func(t *testing.T) {
   673  			for _, dtc := range dateTestData {
   674  				dtc.testParseDate(t, dtc.s, mode)
   675  
   676  				// Combine times with dates to create timestamps.
   677  				for _, ttc := range timeTestData {
   678  					info := fmt.Sprintf("%s %s", dtc.s, ttc.s)
   679  					tstc := dtc.concatTime(ttc)
   680  					tstc.testParseDate(t, info, mode)
   681  					tstc.testParseTime(t, info, mode)
   682  					tstc.testParseTimestamp(t, info, mode)
   683  				}
   684  			}
   685  
   686  			// Test some other timestamps formats we can't create
   687  			// by just concatenating a date + time string.
   688  			for _, ttc := range timestampTestData {
   689  				ttc.testParseTime(t, ttc.s, mode)
   690  			}
   691  		})
   692  	}
   693  
   694  	t.Run("ParseTime", func(t *testing.T) {
   695  		for _, ttc := range timeTestData {
   696  			ttc.testParseTime(t, ttc.s, 0 /* mode */)
   697  		}
   698  	})
   699  }
   700  
   701  // BenchmarkParseTimestampComparison makes a single-pass comparison
   702  // between pgdate.ParseTimestamp() and time.ParseInLocation().
   703  // It bears repeating that ParseTimestamp() can handle all formats
   704  // in a single go, whereas ParseInLocation() would require repeated
   705  // calls in order to try a number of different formats.
   706  func BenchmarkParseTimestampComparison(b *testing.B) {
   707  	// Just a date
   708  	bench(b, "2006-01-02", "2003-06-12", "")
   709  
   710  	// Just a date
   711  	bench(b, "2006-01-02 15:04:05", "2003-06-12 01:02:03", "")
   712  
   713  	// This is the standard wire format.
   714  	bench(b, "2006-01-02 15:04:05.999999999Z07:00", "2003-06-12 04:05:06.789-04:00", "")
   715  
   716  	// 2006-01-02 15:04:05.999999999Z07:00
   717  	bench(b, time.RFC3339Nano, "2000-01-01T02:02:02.567+09:30", "")
   718  
   719  	// Show what happens when a named TZ is used.
   720  	bench(b, "2006-01-02 15:04:05.999999999", "2003-06-12 04:05:06.789", "America/New_York")
   721  }
   722  
   723  // bench compares our ParseTimestamp to ParseInLocation, optionally
   724  // chained with a time.LoadLocation() for resolving named zones.
   725  // The layout parameter is only used for time.ParseInLocation().
   726  // When a named timezone is used, it must be passed via locationName
   727  // so that it may be resolved to a time.Location. It will be
   728  // appended to the string being benchmarked by pgdate.ParseTimestamp().
   729  func bench(b *testing.B, layout string, s string, locationName string) {
   730  	b.Run(strings.TrimSpace(s+" "+locationName), func(b *testing.B) {
   731  		b.Run("ParseTimestamp", func(b *testing.B) {
   732  			benchS := s
   733  			if locationName != "" {
   734  				benchS += " " + locationName
   735  			}
   736  			bytes := int64(len(benchS))
   737  
   738  			b.RunParallel(func(pb *testing.PB) {
   739  				for pb.Next() {
   740  					if _, err := pgdate.ParseTimestamp(time.Time{}, 0, benchS); err != nil {
   741  						b.Fatal(err)
   742  					}
   743  					b.SetBytes(bytes)
   744  				}
   745  			})
   746  		})
   747  
   748  		b.Run("ParseInLocation", func(b *testing.B) {
   749  			bytes := int64(len(s))
   750  			b.RunParallel(func(pb *testing.PB) {
   751  				for pb.Next() {
   752  					loc := time.UTC
   753  					if locationName != "" {
   754  						var err error
   755  						loc, err = time.LoadLocation(locationName)
   756  						if err != nil {
   757  							b.Fatal(err)
   758  						}
   759  					}
   760  					if _, err := time.ParseInLocation(layout, s, loc); err != nil {
   761  						b.Fatal(err)
   762  					}
   763  					b.SetBytes(bytes)
   764  				}
   765  			})
   766  		})
   767  	})
   768  }
   769  
   770  // check is a helper function to compare expected and actual
   771  // outputs and error conditions.
   772  func check(t testing.TB, info string, expTime time.Time, expErr bool, res time.Time, err error) {
   773  	t.Helper()
   774  
   775  	if err == nil {
   776  		if expErr {
   777  			t.Errorf("%s: expected error, but succeeded %s", info, res)
   778  		} else if !res.Equal(expTime) {
   779  			t.Errorf("%s: expected %s, got %s", info, expTime, res)
   780  		}
   781  	} else if !expErr {
   782  		t.Errorf("%s: unexpected error: %v", info, err)
   783  	}
   784  }
   785  
   786  // crossCheck executes the parsing on a remote sql connection.
   787  func (td timeData) crossCheck(
   788  	t *testing.T, info string, kind, s string, mode pgdate.ParseMode, expTime time.Time, expErr bool,
   789  ) {
   790  	if db == nil {
   791  		return
   792  	}
   793  
   794  	switch {
   795  	case db == nil:
   796  		return
   797  	case td.unimplemented:
   798  		return
   799  	case td.expectCrossErr:
   800  		expErr = true
   801  	}
   802  
   803  	info = fmt.Sprintf("%s cross-check", info)
   804  	tx, err := db.Begin()
   805  	if err != nil {
   806  		t.Fatalf("%s: %v", info, err)
   807  	}
   808  
   809  	defer func() {
   810  		if err := tx.Rollback(); err != nil {
   811  			t.Fatalf("%s: %v", info, err)
   812  		}
   813  	}()
   814  
   815  	if _, err := db.Exec("set time zone 'UTC'"); err != nil {
   816  		t.Fatalf("%s: %v", info, err)
   817  	}
   818  
   819  	var style string
   820  	switch mode {
   821  	case pgdate.ParseModeMDY:
   822  		style = "MDY"
   823  	case pgdate.ParseModeDMY:
   824  		style = "DMY"
   825  	case pgdate.ParseModeYMD:
   826  		style = "YMD"
   827  	}
   828  	if _, err := db.Exec(fmt.Sprintf("set datestyle='%s'", style)); err != nil {
   829  		t.Fatalf("%s: %v", info, err)
   830  	}
   831  
   832  	row := db.QueryRow(fmt.Sprintf("select '%s'::%s", s, kind))
   833  	var ret time.Time
   834  	if err := row.Scan(&ret); err == nil {
   835  		switch {
   836  		case expErr:
   837  			t.Errorf("%s: expected error, got %s", info, ret)
   838  		case ret.Round(td.allowCrossDelta).Equal(expTime.Round(td.allowCrossDelta)):
   839  			// Got expected value.
   840  		default:
   841  			t.Errorf("%s: expected %s, got %s", info, expTime, ret)
   842  		}
   843  	} else {
   844  		switch {
   845  		case expErr:
   846  			// Got expected error.
   847  		case kind == "time", kind == "timetz":
   848  			// Our parser is quite a bit more lenient than the
   849  			// PostgreSQL 10.5 implementation. For instance:
   850  			// '1999.123 12:54 PM +11'::timetz --> fail
   851  			// '1999.123 12:54 PM America/New_York'::timetz --> OK
   852  			// Trying to run this down is too much of a time-sink,
   853  			// and as long as we're not producing erroneous values,
   854  			// it's reasonable to treat cases where we can parse,
   855  			// but pg doesn't as a soft failure.
   856  		default:
   857  			t.Errorf(`%s: unexpected error from "%s": %s`, info, s, err)
   858  		}
   859  	}
   860  }