github.com/cockroachdb/cockroachdb-parser@v0.23.3-0.20240213214944-911057d40c9a/pkg/util/timeutil/pgdate/parsing.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 12 13 import ( 14 "strings" 15 "time" 16 17 "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" 18 "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" 19 "github.com/cockroachdb/cockroachdb-parser/pkg/util/timeutil" 20 "github.com/cockroachdb/errors" 21 ) 22 23 // Various keywords that appear in timestamps. 24 const ( 25 // Alias for UTC. No idea who actually uses this. 26 keywordAllBalls = "allballs" 27 keywordAM = "am" 28 keywordEpoch = "epoch" 29 keywordEraAD = "ad" 30 keywordEraBC = "bc" 31 keywordEraBCE = "bce" 32 keywordEraCE = "ce" 33 // Alias for UTC. 34 keywordGMT = "gmt" 35 keywordInfinity = "infinity" 36 keywordNow = "now" 37 keywordPM = "pm" 38 keywordToday = "today" 39 keywordTomorrow = "tomorrow" 40 keywordUTC = "utc" 41 keywordYesterday = "yesterday" 42 // Alias for UTC. 43 keywordZ = "z" 44 // Alias for UTC. 45 keywordZulu = "zulu" 46 ) 47 48 // Commonly-used collections of fields. 49 var ( 50 dateFields = newFieldSet(fieldYear, fieldMonth, fieldDay, fieldEra) 51 dateRequiredFields = newFieldSet(fieldYear, fieldMonth, fieldDay) 52 53 timeFields = newFieldSet( 54 fieldHour, fieldMinute, fieldSecond, fieldNanos, fieldMeridian, 55 fieldTZHour, fieldTZMinute, fieldTZSecond) 56 timeRequiredFields = newFieldSet(fieldHour, fieldMinute) 57 58 db2TimeRequiredFields = newFieldSet(fieldHour, fieldMinute, fieldSecond) 59 60 dateTimeFields = dateFields.AddAll(timeFields) 61 62 tzFields = newFieldSet(fieldTZHour, fieldTZMinute, fieldTZSecond) 63 ) 64 65 // These are sentinel values for handling special values: 66 // https://www.postgresql.org/docs/10/static/datatype-datetime.html#DATATYPE-DATETIME-SPECIAL-TABLE 67 var ( 68 TimeEpoch = timeutil.Unix(0, 0) 69 // TimeInfinity represents the "highest" possible time. 70 // TODO (#41564): this should actually behave as infinity, i.e. any operator 71 // leaves this as infinity. This time should always be greater than any other time. 72 // We should probably use the next microsecond after this value, i.e. timeutil.Unix(9224318016000, 0). 73 // Postgres uses math.MaxInt64 microseconds as the infinity value. 74 // See: https://github.com/postgres/postgres/blob/42aa1f0ab321fd43cbfdd875dd9e13940b485900/src/include/datatype/timestamp.h#L107. 75 TimeInfinity = timeutil.Unix(9224318016000-1, 999999000) 76 // TimeNegativeInfinity represents the "lowest" possible time. 77 // TODO (#41564): this should actually behave as -infinity, i.e. any operator 78 // leaves this as -infinity. This time should always be less than any other time. 79 // We should probably use the next microsecond before this value, i.e. timeutil.Unix(9224318016000-1, 999999000). 80 // Postgres uses math.MinInt64 microseconds as the -infinity value. 81 // See: https://github.com/postgres/postgres/blob/42aa1f0ab321fd43cbfdd875dd9e13940b485900/src/include/datatype/timestamp.h#L107. 82 TimeNegativeInfinity = timeutil.Unix(-210866803200, 0) 83 ) 84 85 type ParseHelper struct { 86 fe fieldExtract 87 } 88 89 // ParseDate converts a string into Date. 90 // 91 // Any specified timezone is inconsequential. Examples: 92 // - "now": parses to the local date (in the current timezone) 93 // - "2020-06-26 01:09:15.511971": parses to '2020-06-26' 94 // - "2020-06-26 01:09:15.511971-05": parses to '2020-06-26' 95 // 96 // The dependsOnContext return value indicates if we had to consult the given 97 // `now` value (either for the time or the local timezone). 98 // 99 // Memory allocations can be avoided by passing ParseHelper which can be re-used 100 // across calls for batch parsing purposes, otherwise it can be nil. 101 func ParseDate( 102 now time.Time, dateStyle DateStyle, s string, h *ParseHelper, 103 ) (_ Date, dependsOnContext bool, _ error) { 104 if h == nil { 105 h = &ParseHelper{} 106 } 107 h.fe = fieldExtract{ 108 currentTime: now, 109 dateStyle: dateStyle, 110 required: dateRequiredFields, 111 // We allow time fields to be provided since they occur after 112 // the date fields that we're really looking for and for 113 // time values like 24:00:00, would push into the next day. 114 wanted: dateTimeFields, 115 } 116 117 if err := h.fe.Extract(s); err != nil { 118 return Date{}, false, parseError(err, "date", s) 119 } 120 date, err := h.fe.MakeDate() 121 return date, h.fe.currentTimeUsed, err 122 } 123 124 // ParseTime converts a string into a time value on the epoch day. 125 // 126 // The dependsOnContext return value indicates if we had to consult the given 127 // `now` value (either for the time or the local timezone). 128 // 129 // Memory allocations can be avoided by passing ParseHelper which can be re-used 130 // across calls for batch parsing purposes, otherwise it can be nil. 131 func ParseTime( 132 now time.Time, dateStyle DateStyle, s string, h *ParseHelper, 133 ) (_ time.Time, dependsOnContext bool, _ error) { 134 if h == nil { 135 h = &ParseHelper{} 136 } 137 h.fe = fieldExtract{ 138 currentTime: now, 139 required: timeRequiredFields, 140 wanted: timeFields, 141 } 142 143 if err := h.fe.Extract(s); err != nil { 144 // It's possible that the user has given us a complete 145 // timestamp string; let's try again, accepting more fields. 146 h.fe = fieldExtract{ 147 currentTime: now, 148 dateStyle: dateStyle, 149 required: timeRequiredFields, 150 wanted: dateTimeFields, 151 } 152 153 if err := h.fe.Extract(s); err != nil { 154 return TimeEpoch, false, parseError(err, "time", s) 155 } 156 } 157 res := h.fe.MakeTime() 158 return res, h.fe.currentTimeUsed, nil 159 } 160 161 // ParseTimeWithoutTimezone converts a string into a time value on the epoch 162 // day, dropping any timezone information. The returned time always has UTC 163 // location. 164 // 165 // Any specified timezone is inconsequential. Examples: 166 // - "now": parses to the local time of day (in the current timezone) 167 // - "01:09:15.511971" and "01:09:15.511971-05" parse to the same result 168 // 169 // The dependsOnContext return value indicates if we had to consult the given 170 // `now` value (either for the time or the local timezone). 171 func ParseTimeWithoutTimezone( 172 now time.Time, dateStyle DateStyle, s string, 173 ) (_ time.Time, dependsOnContext bool, _ error) { 174 fe := fieldExtract{ 175 currentTime: now, 176 required: timeRequiredFields, 177 wanted: timeFields, 178 } 179 180 if err := fe.Extract(s); err != nil { 181 // It's possible that the user has given us a complete 182 // timestamp string; let's try again, accepting more fields. 183 fe = fieldExtract{ 184 currentTime: now, 185 dateStyle: dateStyle, 186 required: timeRequiredFields, 187 wanted: dateTimeFields, 188 } 189 190 if err := fe.Extract(s); err != nil { 191 return TimeEpoch, false, parseError(err, "time", s) 192 } 193 } 194 res := fe.MakeTimeWithoutTimezone() 195 return res, fe.currentTimeUsed, nil 196 } 197 198 // ParseTimestamp converts a string into a timestamp. 199 // 200 // The dependsOnContext return value indicates if we had to consult the given 201 // `now` value (either for the time or the local timezone). 202 func ParseTimestamp( 203 now time.Time, dateStyle DateStyle, s string, 204 ) (_ time.Time, dependsOnContext bool, _ error) { 205 fe := fieldExtract{ 206 dateStyle: dateStyle, 207 currentTime: now, 208 // A timestamp only actually needs a date component; the time 209 // would be midnight. 210 required: dateRequiredFields, 211 wanted: dateTimeFields, 212 } 213 214 if err := fe.Extract(s); err != nil { 215 return TimeEpoch, false, parseError(err, "timestamp", s) 216 } 217 res := fe.MakeTimestamp() 218 return res, fe.currentTimeUsed, nil 219 } 220 221 // ParseTimestampWithoutTimezone converts a string into a timestamp, stripping 222 // away any timezone information. Any specified timezone is inconsequential. The 223 // returned time always has UTC location. 224 // 225 // For example, all these inputs return 2020-06-26 01:02:03 +0000 UTC: 226 // - '2020-06-26 01:02:03'; 227 // - '2020-06-26 01:02:03+04'; 228 // - 'now', if the local time (in the current timezone) is 229 // 2020-06-26 01:02:03. Note that this does not represent the same time 230 // instant, but the one that "reads" the same in UTC. 231 // 232 // The dependsOnContext return value indicates if we had to consult the given 233 // `now` value (either for the time or the local timezone). 234 func ParseTimestampWithoutTimezone( 235 now time.Time, dateStyle DateStyle, s string, 236 ) (_ time.Time, dependsOnContext bool, _ error) { 237 fe := fieldExtract{ 238 dateStyle: dateStyle, 239 currentTime: now, 240 // A timestamp only actually needs a date component; the time 241 // would be midnight. 242 required: dateRequiredFields, 243 wanted: dateTimeFields, 244 } 245 246 if err := fe.Extract(s); err != nil { 247 return TimeEpoch, false, parseError(err, "timestamp", s) 248 } 249 res := fe.MakeTimestampWithoutTimezone() 250 return res, fe.currentTimeUsed, nil 251 } 252 253 // badFieldPrefixError constructs an error with pg code InvalidDatetimeFormat. 254 func badFieldPrefixError(field field, prefix rune) error { 255 return inputErrorf("unexpected separator '%s' for field %s", 256 string(prefix), errors.Safe(field.Pretty())) 257 } 258 259 // inputErrorf returns an error with pg code InvalidDatetimeFormat. 260 func inputErrorf(format string, args ...interface{}) error { 261 err := errors.Newf(format, args...) 262 return pgerror.WithCandidateCode(err, pgcode.InvalidDatetimeFormat) 263 } 264 265 // outOfRangeError returns an error with pg code DatetimeFieldOverflow. 266 func outOfRangeError(field string, val int) error { 267 err := errors.Newf("field %s value %d is out of range", errors.Safe(field), errors.Safe(val)) 268 return errors.WithHint( 269 pgerror.WithCandidateCode(err, pgcode.DatetimeFieldOverflow), 270 `Perhaps you need a different "datestyle" setting.`, 271 ) 272 } 273 274 // parseError ensures that any error we return to the client will 275 // be some kind of error with a pg code. 276 func parseError(err error, kind string, s string) error { 277 return pgerror.WithCandidateCode( 278 errors.Wrapf(err, "parsing as type %s", errors.Safe(kind)), 279 pgcode.InvalidDatetimeFormat) 280 } 281 282 // DefaultDateStyle returns the default datestyle for Postgres. 283 func DefaultDateStyle() DateStyle { 284 return DateStyle{ 285 Order: Order_MDY, 286 Style: Style_ISO, 287 } 288 } 289 290 // ParseDateStyle parses a given DateStyle, modifying the existingDateStyle 291 // as appropriate. This is because specifying just Style or Order will leave 292 // the other field unchanged. 293 func ParseDateStyle(s string, existingDateStyle DateStyle) (DateStyle, error) { 294 ds := existingDateStyle 295 fields := strings.Split(s, ",") 296 for _, field := range fields { 297 field = strings.ToLower(strings.TrimSpace(field)) 298 switch field { 299 case "iso": 300 ds.Style = Style_ISO 301 case "german": 302 ds.Style = Style_GERMAN 303 case "sql": 304 ds.Style = Style_SQL 305 case "postgres": 306 ds.Style = Style_POSTGRES 307 case "ymd": 308 ds.Order = Order_YMD 309 case "mdy": 310 ds.Order = Order_MDY 311 case "dmy": 312 ds.Order = Order_DMY 313 default: 314 return ds, pgerror.Newf(pgcode.InvalidParameterValue, "unknown DateStyle parameter: %s", field) 315 } 316 } 317 return ds, nil 318 }