github.com/dolthub/go-mysql-server@v0.18.0/sql/events.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sql 16 17 import ( 18 "fmt" 19 "regexp" 20 "strconv" 21 "strings" 22 "time" 23 24 "gopkg.in/src-d/go-errors.v1" 25 26 gmstime "github.com/dolthub/go-mysql-server/internal/time" 27 ) 28 29 const EventDateSpaceTimeFormat = "2006-01-02 15:04:05" 30 31 // EventSchedulerStatement represents a SQL statement that requires a EventScheduler 32 // (e.g. CREATE / ALTER / DROP EVENT and DROP DATABASE). 33 type EventSchedulerStatement interface { 34 Node 35 // WithEventScheduler returns a new instance of this EventSchedulerStatement, 36 // with the event scheduler notifier configured. 37 WithEventScheduler(controller EventScheduler) Node 38 } 39 40 // EventScheduler is an interface used for notifying the EventSchedulerStatus 41 // for querying any events related statements. This allows plan Nodes to communicate 42 // to the EventSchedulerStatus. 43 type EventScheduler interface { 44 // AddEvent is called when there is an event created at runtime. 45 AddEvent(ctx *Context, edb EventDatabase, event EventDefinition) 46 // UpdateEvent is called when there is an event altered at runtime. 47 UpdateEvent(ctx *Context, edb EventDatabase, orgEventName string, event EventDefinition) 48 // RemoveEvent is called when there is an event dropped at runtime. This function 49 // removes the given event if it exists in the enabled events list of the EventSchedulerStatus. 50 RemoveEvent(dbName, eventName string) 51 // RemoveSchemaEvents is called when there is a database dropped at runtime. This function 52 // removes all events of given database that exist in the enabled events list of the EventSchedulerStatus. 53 RemoveSchemaEvents(dbName string) 54 } 55 56 // EventDefinition describes a scheduled event. 57 type EventDefinition struct { 58 // The name of this event. Event names in a database are unique. 59 Name string 60 // The SQL statements to be executed when this event is executed. 61 EventBody string 62 // The timezone offset the event was created or last altered at. 63 TimezoneOffset string 64 // The enabled or disabled status of this event. 65 Status string 66 // The user or account who created this scheduled event. 67 Definer string 68 // The SQL_MODE in effect when this event was created. 69 SqlMode string 70 // The time at which the event was created. 71 CreatedAt time.Time 72 // The time at which the event was last altered. 73 LastAltered time.Time 74 // The time at which the event was last executed. 75 LastExecuted time.Time 76 77 /* Fields parsed from the CREATE EVENT statement */ 78 Comment string 79 OnCompletionPreserve bool 80 HasExecuteAt bool 81 ExecuteAt time.Time 82 ExecuteEvery string 83 Starts time.Time // STARTS is always defined when EVERY is defined. 84 HasEnds bool 85 Ends time.Time 86 } 87 88 // ConvertTimesFromUTCToTz returns a new EventDefinition with all its time values converted 89 // from UTC TZ to the given TZ. This function should only be used when needing to display 90 // data that includes the time values in string format for such as SHOW EVENTS or 91 // SHOW CREATE EVENT statements. 92 func (e *EventDefinition) ConvertTimesFromUTCToTz(tz string) *EventDefinition { 93 ne := *e 94 if ne.HasExecuteAt { 95 t, ok := gmstime.ConvertTimeZone(e.ExecuteAt, "+00:00", tz) 96 if ok { 97 ne.ExecuteAt = t 98 } 99 } else { 100 t, ok := gmstime.ConvertTimeZone(e.Starts, "+00:00", tz) 101 if ok { 102 ne.Starts = t 103 } 104 if ne.HasEnds { 105 t, ok = gmstime.ConvertTimeZone(e.Ends, "+00:00", tz) 106 if ok { 107 ne.Ends = t 108 } 109 } 110 } 111 112 t, ok := gmstime.ConvertTimeZone(e.CreatedAt, "+00:00", tz) 113 if ok { 114 ne.CreatedAt = t 115 } 116 t, ok = gmstime.ConvertTimeZone(e.LastAltered, "+00:00", tz) 117 if ok { 118 ne.LastAltered = t 119 } 120 t, ok = gmstime.ConvertTimeZone(e.LastExecuted, "+00:00", tz) 121 if ok { 122 ne.LastExecuted = t 123 } 124 return &ne 125 } 126 127 // GetNextExecutionTime returns the next execution time for the event, which depends on AT 128 // or EVERY field of EventDefinition. It also returns whether the event is expired. 129 func (e *EventDefinition) GetNextExecutionTime(curTime time.Time) (time.Time, bool, error) { 130 if e.HasExecuteAt { 131 return e.ExecuteAt, e.ExecuteAt.Sub(curTime).Seconds() <= -1, nil 132 } else { 133 timeDur, err := getTimeDurationFromEveryInterval(e.ExecuteEvery) 134 if err != nil { 135 return time.Time{}, true, err 136 } 137 // check for last executed, if not set, get the next time by incrementing the start time by interval 138 // use 'last executed' time if the event was executed before; otherwise, use 'starts' time 139 startTime := e.Starts 140 if !e.LastExecuted.IsZero() && e.LastExecuted.Sub(e.Starts).Seconds() > 0 { 141 startTime = e.LastExecuted 142 } 143 144 // if startTime > curTime, then event hasn't executed yet, so execute at startTime 145 if startTime.Sub(curTime).Seconds() > 0 { 146 return startTime, false, nil 147 } 148 // if endTime is defined and endTime < curTime, then event is ended 149 if e.HasEnds && e.Ends.Sub(curTime).Seconds() < 0 { 150 return time.Time{}, true, nil 151 } 152 153 diffToNext := (int64(curTime.Sub(startTime).Seconds()/timeDur.Seconds()) + 1) * int64(timeDur.Seconds()) 154 nextTime := startTime.Add(time.Duration(diffToNext) * time.Second) 155 // sanity check 156 for nextTime.Sub(curTime).Seconds() < 0 { 157 nextTime = nextTime.Add(timeDur) 158 } 159 // if the next execution time is past the endTime, then the event is expired. 160 if e.HasEnds && e.Ends.Sub(nextTime).Seconds() < 0 { 161 return time.Time{}, true, nil 162 } 163 return nextTime, false, nil 164 } 165 } 166 167 // CreateEventStatement returns a CREATE EVENT statement for this event. 168 func (e *EventDefinition) CreateEventStatement() string { 169 stmt := "CREATE" 170 if e.Definer != "" { 171 stmt = fmt.Sprintf("%s DEFINER = %s", stmt, e.Definer) 172 } 173 stmt = fmt.Sprintf("%s EVENT `%s`", stmt, e.Name) 174 175 if e.HasExecuteAt { 176 stmt = fmt.Sprintf("%s ON SCHEDULE AT '%s'", stmt, e.ExecuteAt.Format(EventDateSpaceTimeFormat)) 177 } else { 178 // STARTS should be NOT null regardless of user definition 179 stmt = fmt.Sprintf("%s ON SCHEDULE EVERY %s STARTS '%s'", stmt, e.ExecuteEvery, e.Starts.Format(EventDateSpaceTimeFormat)) 180 if e.HasEnds { 181 stmt = fmt.Sprintf("%s ENDS '%s'", stmt, e.Ends.Format(EventDateSpaceTimeFormat)) 182 } 183 } 184 185 if e.OnCompletionPreserve { 186 stmt = fmt.Sprintf("%s ON COMPLETION PRESERVE", stmt) 187 } else { 188 stmt = fmt.Sprintf("%s ON COMPLETION NOT PRESERVE", stmt) 189 } 190 191 stmt = fmt.Sprintf("%s %s", stmt, e.Status) 192 193 if e.Comment != "" { 194 stmt = fmt.Sprintf("%s COMMENT '%s'", stmt, e.Comment) 195 } 196 197 return fmt.Sprintf("%s DO %s", stmt, e.EventBody) 198 } 199 200 // getTimeDurationFromEveryInterval returns time.Duration converting the given EVERY interval. 201 func getTimeDurationFromEveryInterval(every string) (time.Duration, error) { 202 everyInterval, err := EventOnScheduleEveryIntervalFromString(every) 203 if err != nil { 204 return 0, err 205 } 206 hours := everyInterval.Years*8766 + everyInterval.Months*730 + everyInterval.Days*24 + everyInterval.Hours 207 timeDur := time.Duration(hours)*time.Hour + time.Duration(everyInterval.Minutes)*time.Minute + time.Duration(everyInterval.Seconds)*time.Second 208 209 return timeDur, nil 210 } 211 212 // EventStatus represents an event status that is defined for an event. 213 type EventStatus byte 214 215 const ( 216 EventStatus_Enable EventStatus = iota 217 EventStatus_Disable 218 EventStatus_DisableOnSlave 219 ) 220 221 // String returns the original SQL representation. 222 func (e EventStatus) String() string { 223 switch e { 224 case EventStatus_Enable: 225 return "ENABLE" 226 case EventStatus_Disable: 227 return "DISABLE" 228 case EventStatus_DisableOnSlave: 229 return "DISABLE ON SLAVE" 230 default: 231 panic(fmt.Errorf("invalid event status value `%d`", byte(e))) 232 } 233 } 234 235 // EventStatusFromString returns EventStatus based on the given string value. 236 // This function is used in Dolt to get EventStatus value for the EventDefinition. 237 func EventStatusFromString(status string) (EventStatus, error) { 238 switch strings.ToLower(status) { 239 case "enable": 240 return EventStatus_Enable, nil 241 case "disable": 242 return EventStatus_Disable, nil 243 case "disable on slave": 244 return EventStatus_DisableOnSlave, nil 245 default: 246 // use disable as default to be safe 247 return EventStatus_Disable, fmt.Errorf("invalid event status value: `%s`", status) 248 } 249 } 250 251 // EventOnScheduleEveryInterval is used to store ON SCHEDULE EVERY clause's interval definition. 252 // It is equivalent of expression.TimeDelta without microseconds field. 253 type EventOnScheduleEveryInterval struct { 254 Years int64 255 Months int64 256 Days int64 257 Hours int64 258 Minutes int64 259 Seconds int64 260 } 261 262 func NewEveryInterval(y, mo, d, h, mi, s int64) *EventOnScheduleEveryInterval { 263 return &EventOnScheduleEveryInterval{ 264 Years: y, 265 Months: mo, 266 Days: d, 267 Hours: h, 268 Minutes: mi, 269 Seconds: s, 270 } 271 } 272 273 // GetIntervalValAndField returns ON SCHEDULE EVERY clause's interval value and field type in string format 274 // (e.g. returns "'1:2'" and "MONTH_DAY" for 1 month and 2 day or returns "4" and "HOUR" for 4 hour intervals). 275 func (e *EventOnScheduleEveryInterval) GetIntervalValAndField() (string, string) { 276 if e == nil { 277 return "", "" 278 } 279 280 var val, field []string 281 if e.Years != 0 { 282 val = append(val, fmt.Sprintf("%v", e.Years)) 283 field = append(field, "YEAR") 284 } 285 if e.Months != 0 { 286 val = append(val, fmt.Sprintf("%v", e.Months)) 287 field = append(field, "MONTH") 288 } 289 if e.Days != 0 { 290 val = append(val, fmt.Sprintf("%v", e.Days)) 291 field = append(field, "DAY") 292 } 293 if e.Hours != 0 { 294 val = append(val, fmt.Sprintf("%v", e.Hours)) 295 field = append(field, "HOUR") 296 } 297 if e.Minutes != 0 { 298 val = append(val, fmt.Sprintf("%v", e.Minutes)) 299 field = append(field, "MINUTE") 300 } 301 if e.Seconds != 0 { 302 val = append(val, fmt.Sprintf("%v", e.Seconds)) 303 field = append(field, "SECOND") 304 } 305 306 if len(val) == 0 { 307 return "", "" 308 } else if len(val) == 1 { 309 return val[0], field[0] 310 } 311 312 return fmt.Sprintf("'%s'", strings.Join(val, ":")), strings.Join(field, "_") 313 } 314 315 // EventOnScheduleEveryIntervalFromString returns *EventOnScheduleEveryInterval parsing given interval string 316 // such as `2 DAY` or `'1:2' MONTH_DAY`. This function is used in Dolt to construct EventOnScheduleEveryInterval value 317 // for the EventDefinition. 318 func EventOnScheduleEveryIntervalFromString(every string) (*EventOnScheduleEveryInterval, error) { 319 errCannotParseEveryInterval := fmt.Errorf("cannot parse ON SCHEDULE EVERY interval: `%s`", every) 320 strs := strings.Split(every, " ") 321 if len(strs) != 2 { 322 return nil, errCannotParseEveryInterval 323 } 324 intervalVal := strs[0] 325 intervalField := strs[1] 326 327 intervalVal = strings.TrimSuffix(strings.TrimPrefix(intervalVal, "'"), "'") 328 iVals := strings.Split(intervalVal, ":") 329 iFields := strings.Split(intervalField, "_") 330 331 if len(iVals) != len(iFields) { 332 return nil, errCannotParseEveryInterval 333 } 334 335 var interval = &EventOnScheduleEveryInterval{} 336 for i, val := range iVals { 337 n, err := strconv.ParseInt(val, 10, 64) 338 if err != nil { 339 return nil, errCannotParseEveryInterval 340 } 341 switch iFields[i] { 342 case "YEAR": 343 interval.Years = n 344 case "MONTH": 345 interval.Months = n 346 case "DAY": 347 interval.Days = n 348 case "HOUR": 349 interval.Hours = n 350 case "MINUTE": 351 interval.Minutes = n 352 case "SECOND": 353 interval.Seconds = n 354 default: 355 return nil, errCannotParseEveryInterval 356 } 357 } 358 359 return interval, nil 360 } 361 362 // ------------------------- 363 // Events datetime parsing 364 // ------------------------- 365 366 var ErrIncorrectValue = errors.NewKind("Incorrect %s value: '%s'") 367 var dateRegex = regexp.MustCompile(`(?m)^(\d{1,4})-(\d{1,2})-(\d{1,2})(.*)$`) 368 var timeRegex = regexp.MustCompile(`(?m)^([ T])?(\d{1,2})?(:)?(\d{1,2})?(:)?(\d{1,2})?(\.)?(\d{1,6})?(.*)$`) 369 var tzRegex = regexp.MustCompile(`(?m)^([+\-])(\d{2}):(\d{2})$`) 370 371 // GetTimeValueFromStringInput returns time.Time in system timezone (SYSTEM = time.Now().Location()). 372 // evaluating valid MySQL datetime and timestamp formats. 373 func GetTimeValueFromStringInput(field, t string) (time.Time, error) { 374 // TODO: the time value should be in session timezone rather than system timezone. 375 sessTz := gmstime.SystemTimezoneOffset() 376 377 // For MySQL datetime format, it accepts any valid date format 378 // and tries parsing time part first and timezone part if time part is valid. 379 // Otherwise, any invalid time or timezone part is truncated and gives warning. 380 // TODO: It seems like we should be able to reuse the timestamp parsing logic from Datetime.Convert. 381 // Do we need to reimplement this here? 382 dt := strings.Split(t, "-") 383 if len(dt) > 1 { 384 var year, month, day, hour, minute, second int 385 var timePart, tzPart string 386 var ok bool 387 var inputTz = sessTz 388 // FIRST try to get date part 389 year, month, day, timePart, ok = getDatePart(t) 390 if !ok { 391 return time.Time{}, ErrIncorrectValue.New(field, t) 392 } 393 // Then time part 394 if timePart != "" { 395 hour, minute, second, tzPart, ok = getTimePart(timePart) 396 if !ok { 397 return time.Time{}, ErrIncorrectValue.New(field, t) 398 } 399 } 400 // Then timezone part 401 if tzPart != "" { 402 if tzPart[0] != '+' && tzPart[0] != '-' { 403 // TODO: warning: Truncated incorrect datetime value: '...' 404 } else { 405 inputTz, ok = getTimezonePart(tzPart) 406 if !ok { 407 return time.Time{}, ErrIncorrectValue.New(field, t) 408 } 409 } 410 } 411 412 datetimeVal := fmt.Sprintf("%4d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second) 413 tVal, err := time.Parse(EventDateSpaceTimeFormat, datetimeVal) 414 if err != nil { 415 return time.Time{}, fmt.Errorf("invalid time zone: %s", sessTz) 416 } 417 418 // convert the time value to the session timezone for display and storage 419 tVal, ok = gmstime.ConvertTimeZone(tVal, inputTz, sessTz) 420 if !ok { 421 return time.Time{}, fmt.Errorf("invalid time zone: %s", sessTz) 422 } 423 return tVal, nil 424 } else { 425 // TODO: support timestamp input parsing (e.g. 2023526...) 426 return time.Time{}, fmt.Errorf("timestamp input parsing not supported yet") 427 } 428 } 429 430 func getDatePart(s string) (int, int, int, string, bool) { 431 matches := dateRegex.FindStringSubmatch(s) 432 if matches == nil || len(matches) != 5 { 433 return 0, 0, 0, "", false 434 } 435 436 year, ok := validateYear(getInt(matches[1])) 437 return year, getInt(matches[2]), getInt(matches[3]), matches[4], ok 438 } 439 440 func getTimePart(t string) (int, int, int, string, bool) { 441 var hour, minute, second int 442 matches := timeRegex.FindStringSubmatch(t) 443 if matches == nil || len(matches) != 10 { 444 return 0, 0, 0, "", false 445 } 446 hour = getInt(matches[2]) 447 if matches[3] == "" { 448 return hour, minute, second, "", true 449 } else if matches[3] != ":" { 450 return 0, 0, 0, "", false 451 } 452 minute = getInt(matches[4]) 453 if matches[5] == "" { 454 return hour, minute, second, "", true 455 } else if matches[5] != ":" { 456 return 0, 0, 0, "", false 457 } 458 second = getInt(matches[6]) 459 // microsecond with dot in front of it is not needed for now 460 //if matches[7] != "." { 461 // return 0, 0, 0, "", false 462 //} 463 //microsecond := matches[8] 464 return hour, minute, second, matches[9], true 465 } 466 467 func getTimezonePart(tz string) (string, bool) { 468 matches := tzRegex.FindStringSubmatch(tz) 469 if len(matches) == 4 { 470 symbol := matches[1] 471 hours := matches[2] 472 mins := matches[3] 473 return fmt.Sprintf("%s%s:%s", symbol, hours, mins), true 474 } else { 475 return "", false 476 } 477 } 478 479 func getInt(s string) int { 480 i, err := strconv.Atoi(s) 481 if err != nil { 482 return 0 483 } 484 return i 485 } 486 487 func validateYear(i int) (int, bool) { 488 if i >= 0 && i <= 69 { 489 return i + 2000, true 490 } else if i >= 70 && i <= 99 { 491 return i + 1900, true 492 } else if i >= 1901 && i < 2155 { 493 return i, true 494 } 495 return 0, false 496 }