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 }