github.com/dolthub/go-mysql-server@v0.18.0/sql/plan/ddl_event.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 plan 16 17 import ( 18 "fmt" 19 "io" 20 "strings" 21 "sync" 22 "time" 23 24 "github.com/dolthub/vitess/go/mysql" 25 26 gmstime "github.com/dolthub/go-mysql-server/internal/time" 27 "github.com/dolthub/go-mysql-server/sql" 28 "github.com/dolthub/go-mysql-server/sql/expression" 29 "github.com/dolthub/go-mysql-server/sql/types" 30 ) 31 32 var _ sql.Node = (*CreateEvent)(nil) 33 var _ sql.Expressioner = (*CreateEvent)(nil) 34 var _ sql.Databaser = (*CreateEvent)(nil) 35 var _ sql.EventSchedulerStatement = (*CreateEvent)(nil) 36 37 type CreateEvent struct { 38 ddlNode 39 EventName string 40 Definer string 41 At *OnScheduleTimestamp 42 Every *expression.Interval 43 Starts *OnScheduleTimestamp 44 Ends *OnScheduleTimestamp 45 OnCompPreserve bool 46 Status sql.EventStatus 47 Comment string 48 DefinitionString string 49 DefinitionNode sql.Node 50 IfNotExists bool 51 // eventScheduler is used to notify EventSchedulerStatus of the event creation 52 eventScheduler sql.EventScheduler 53 } 54 55 // NewCreateEvent returns a *CreateEvent node. 56 func NewCreateEvent( 57 db sql.Database, 58 name, definer string, 59 at, starts, ends *OnScheduleTimestamp, 60 every *expression.Interval, 61 onCompletionPreserve bool, 62 status sql.EventStatus, 63 comment, definitionString string, 64 definition sql.Node, 65 ifNotExists bool, 66 ) *CreateEvent { 67 return &CreateEvent{ 68 ddlNode: ddlNode{db}, 69 EventName: name, 70 Definer: definer, 71 At: at, 72 Every: every, 73 Starts: starts, 74 Ends: ends, 75 OnCompPreserve: onCompletionPreserve, 76 Status: status, 77 Comment: comment, 78 DefinitionString: definitionString, 79 DefinitionNode: prepareCreateEventDefinitionNode(definition), 80 IfNotExists: ifNotExists, 81 } 82 } 83 84 // Resolved implements the sql.Node interface. 85 func (c *CreateEvent) Resolved() bool { 86 r := c.ddlNode.Resolved() && c.DefinitionNode.Resolved() 87 if c.At != nil { 88 r = r && c.At.Resolved() 89 } else { 90 r = r && c.Every.Resolved() 91 if c.Starts != nil { 92 r = r && c.Starts.Resolved() 93 } 94 if c.Ends != nil { 95 r = r && c.Ends.Resolved() 96 } 97 } 98 return r 99 } 100 101 func (c *CreateEvent) IsReadOnly() bool { 102 return false 103 } 104 105 // Schema implements the sql.Node interface. 106 func (c *CreateEvent) Schema() sql.Schema { 107 return nil 108 } 109 110 // Children implements the sql.Node interface. 111 func (c *CreateEvent) Children() []sql.Node { 112 return []sql.Node{c.DefinitionNode} 113 } 114 115 // WithChildren implements the sql.Node interface. 116 func (c *CreateEvent) WithChildren(children ...sql.Node) (sql.Node, error) { 117 if len(children) != 1 { 118 return nil, sql.ErrInvalidChildrenNumber.New(c, len(children), 1) 119 } 120 121 nc := *c 122 nc.DefinitionNode = prepareCreateEventDefinitionNode(children[0]) 123 124 return &nc, nil 125 } 126 127 // CheckPrivileges implements the interface sql.Node. 128 func (c *CreateEvent) CheckPrivileges(ctx *sql.Context, opChecker sql.PrivilegedOperationChecker) bool { 129 subject := sql.PrivilegeCheckSubject{ 130 Database: c.Db.Name(), 131 } 132 return opChecker.UserHasPrivileges(ctx, sql.NewPrivilegedOperation(subject, sql.PrivilegeType_Event)) 133 } 134 135 // Database implements the sql.Databaser interface. 136 func (c *CreateEvent) Database() sql.Database { 137 return c.Db 138 } 139 140 // WithDatabase implements the sql.Databaser interface. 141 func (c *CreateEvent) WithDatabase(database sql.Database) (sql.Node, error) { 142 ce := *c 143 ce.Db = database 144 return &ce, nil 145 } 146 147 // String implements the sql.Node interface. 148 func (c *CreateEvent) String() string { 149 definer := "" 150 if c.Definer != "" { 151 definer = fmt.Sprintf(" DEFINER = %s", c.Definer) 152 } 153 154 onSchedule := "" 155 if c.At != nil { 156 onSchedule = fmt.Sprintf(" ON SCHEDULE %s", c.At.String()) 157 } else { 158 onSchedule = onScheduleEveryString(c.Every, c.Starts, c.Ends) 159 } 160 161 onCompletion := "" 162 if !c.OnCompPreserve { 163 onCompletion = " ON COMPLETION NOT PRESERVE" 164 } 165 166 comment := "" 167 if c.Comment != "" { 168 comment = fmt.Sprintf(" COMMENT '%s'", c.Comment) 169 } 170 171 return fmt.Sprintf("CREATE%s EVENT %s %s%s%s%s DO %s", 172 definer, c.EventName, onSchedule, onCompletion, c.Status.String(), comment, sql.DebugString(c.DefinitionNode)) 173 } 174 175 // Expressions implements the sql.Expressioner interface. 176 func (c *CreateEvent) Expressions() []sql.Expression { 177 if c.At != nil { 178 return []sql.Expression{c.At} 179 } else { 180 if c.Starts == nil && c.Ends == nil { 181 return []sql.Expression{c.Every} 182 } else if c.Starts == nil { 183 return []sql.Expression{c.Every, c.Ends} 184 } else if c.Ends == nil { 185 return []sql.Expression{c.Every, c.Starts} 186 } else { 187 return []sql.Expression{c.Every, c.Starts, c.Ends} 188 } 189 } 190 } 191 192 // WithExpressions implements the sql.Expressioner interface. 193 func (c *CreateEvent) WithExpressions(e ...sql.Expression) (sql.Node, error) { 194 if len(e) < 1 { 195 return nil, sql.ErrInvalidChildrenNumber.New(c, len(e), "at least 1") 196 } 197 198 nc := *c 199 if c.At != nil { 200 ts, ok := e[0].(*OnScheduleTimestamp) 201 if !ok { 202 return nil, fmt.Errorf("expected `*OnScheduleTimestamp` but got `%T`", e[0]) 203 } 204 nc.At = ts 205 } else { 206 every, ok := e[0].(*expression.Interval) 207 if !ok { 208 return nil, fmt.Errorf("expected `*expression.Interval` but got `%T`", e[0]) 209 } 210 nc.Every = every 211 212 var ts *OnScheduleTimestamp 213 if len(e) > 1 { 214 ts, ok = e[1].(*OnScheduleTimestamp) 215 if !ok { 216 return nil, fmt.Errorf("expected `*OnScheduleTimestamp` but got `%T`", e[1]) 217 } 218 if c.Starts != nil { 219 nc.Starts = ts 220 } else if c.Ends != nil { 221 nc.Ends = ts 222 } 223 } 224 225 if len(e) == 3 { 226 ts, ok = e[2].(*OnScheduleTimestamp) 227 if !ok { 228 return nil, fmt.Errorf("expected `*OnScheduleTimestamp` but got `%T`", e[2]) 229 } 230 nc.Ends = ts 231 } 232 } 233 234 return &nc, nil 235 } 236 237 // RowIter implements the sql.Node interface. 238 func (c *CreateEvent) RowIter(ctx *sql.Context, _ sql.Row) (sql.RowIter, error) { 239 eventCreationTime := ctx.QueryTime() 240 // TODO: event time values are evaluated in 'SYSTEM' TZ for now (should be session TZ) 241 eventDefinition, err := c.GetEventDefinition(ctx, eventCreationTime, eventCreationTime, time.Time{}, gmstime.SystemTimezoneOffset()) 242 if err != nil { 243 return nil, err 244 } 245 246 eventDb, ok := c.Db.(sql.EventDatabase) 247 if !ok { 248 return nil, sql.ErrEventsNotSupported.New(c.Db.Name()) 249 } 250 251 return &createEventIter{ 252 event: eventDefinition, 253 eventDb: eventDb, 254 ifNotExists: c.IfNotExists, 255 eventScheduler: c.eventScheduler, 256 }, nil 257 } 258 259 // WithEventScheduler is used to notify EventSchedulerStatus to update the events list for CREATE EVENT. 260 func (c *CreateEvent) WithEventScheduler(scheduler sql.EventScheduler) sql.Node { 261 nc := *c 262 nc.eventScheduler = scheduler 263 return &nc 264 } 265 266 // GetEventDefinition returns an EventDefinition object with all of its fields populated from the details 267 // of this CREATE EVENT statement. 268 func (c *CreateEvent) GetEventDefinition(ctx *sql.Context, eventCreationTime, lastAltered, lastExecuted time.Time, tz string) (sql.EventDefinition, error) { 269 // TODO: support DISABLE ON SLAVE event status 270 if c.Status == sql.EventStatus_DisableOnSlave && ctx != nil && ctx.Session != nil { 271 ctx.Session.Warn(&sql.Warning{ 272 Level: "Warning", 273 Code: mysql.ERNotSupportedYet, 274 Message: fmt.Sprintf("DISABLE ON SLAVE status is not supported yet, used DISABLE status instead."), 275 }) 276 c.Status = sql.EventStatus_Disable 277 } 278 279 eventDefinition := sql.EventDefinition{ 280 Name: c.EventName, 281 Definer: c.Definer, 282 OnCompletionPreserve: c.OnCompPreserve, 283 Status: c.Status.String(), 284 Comment: c.Comment, 285 EventBody: c.DefinitionString, 286 TimezoneOffset: tz, 287 } 288 289 if c.At != nil { 290 var err error 291 eventDefinition.HasExecuteAt = true 292 eventDefinition.ExecuteAt, err = c.At.EvalTime(ctx, tz) 293 if err != nil { 294 return sql.EventDefinition{}, err 295 } 296 } else { 297 delta, err := c.Every.EvalDelta(ctx, nil) 298 if err != nil { 299 return sql.EventDefinition{}, err 300 } 301 interval := sql.NewEveryInterval(delta.Years, delta.Months, delta.Days, delta.Hours, delta.Minutes, delta.Seconds) 302 iVal, iField := interval.GetIntervalValAndField() 303 eventDefinition.ExecuteEvery = fmt.Sprintf("%s %s", iVal, iField) 304 305 if c.Starts != nil { 306 eventDefinition.Starts, err = c.Starts.EvalTime(ctx, tz) 307 if err != nil { 308 return sql.EventDefinition{}, err 309 } 310 } else { 311 // If STARTS is not defined, it defaults to CURRENT_TIMESTAMP 312 eventDefinition.Starts = eventCreationTime 313 } 314 if c.Ends != nil { 315 eventDefinition.HasEnds = true 316 eventDefinition.Ends, err = c.Ends.EvalTime(ctx, tz) 317 if err != nil { 318 return sql.EventDefinition{}, err 319 } 320 } 321 } 322 323 eventDefinition.CreatedAt = eventCreationTime 324 eventDefinition.LastAltered = lastAltered 325 eventDefinition.LastExecuted = lastExecuted 326 return eventDefinition, nil 327 } 328 329 // prepareCreateEventDefinitionNode fills in any missing ProcedureReference structures for 330 // BeginEndBlocks in the event's definition. 331 func prepareCreateEventDefinitionNode(definition sql.Node) sql.Node { 332 beginEndBlock, ok := definition.(*BeginEndBlock) 333 if !ok { 334 return definition 335 } 336 337 // NOTE: To execute a multi-statement event body in a BeginEndBlock, a ProcedureReference 338 // must be set in the BeginEndBlock, but this currently only gets initialized in the 339 // analyzer for ProcedureCalls, so we initialize it here. 340 // TODO: How does this work for triggers, which would have the same issue; seems like there 341 // should be a cleaner way to handle this 342 beginEndBlock.Pref = expression.NewProcedureReference() 343 344 newChildren := make([]sql.Node, len(beginEndBlock.Children())) 345 for i, child := range beginEndBlock.Children() { 346 newChildren[i] = prepareCreateEventDefinitionNode(child) 347 } 348 newNode, _ := beginEndBlock.WithChildren(newChildren...) 349 return newNode 350 } 351 352 // createEventIter is the row iterator for *CreateEvent. 353 type createEventIter struct { 354 once sync.Once 355 event sql.EventDefinition 356 eventDb sql.EventDatabase 357 ifNotExists bool 358 eventScheduler sql.EventScheduler 359 } 360 361 // Next implements the sql.RowIter interface. 362 func (c *createEventIter) Next(ctx *sql.Context) (sql.Row, error) { 363 run := false 364 c.once.Do(func() { 365 run = true 366 }) 367 if !run { 368 return nil, io.EOF 369 } 370 371 mode := sql.LoadSqlMode(ctx) 372 c.event.SqlMode = mode.String() 373 374 // checks if the defined ENDS time is before STARTS time 375 if c.event.HasEnds { 376 if c.event.Ends.Sub(c.event.Starts).Seconds() < 0 { 377 return nil, fmt.Errorf("ENDS is either invalid or before STARTS") 378 } 379 } 380 381 enabled, err := c.eventDb.SaveEvent(ctx, c.event) 382 if err != nil { 383 if sql.ErrEventAlreadyExists.Is(err) && c.ifNotExists { 384 ctx.Session.Warn(&sql.Warning{ 385 Level: "Note", 386 Code: 1537, 387 Message: fmt.Sprintf(err.Error()), 388 }) 389 return sql.Row{types.NewOkResult(0)}, nil 390 } 391 return nil, err 392 } 393 394 if c.event.HasExecuteAt { 395 // If the event execution time is in the past and is set. 396 if c.event.ExecuteAt.Sub(c.event.CreatedAt).Seconds() <= -1 { 397 if c.event.OnCompletionPreserve && ctx != nil && ctx.Session != nil { 398 // If ON COMPLETION PRESERVE is defined, the event is disabled. 399 c.event.Status = sql.EventStatus_Disable.String() 400 _, err = c.eventDb.UpdateEvent(ctx, c.event.Name, c.event) 401 if err != nil { 402 return nil, err 403 } 404 ctx.Session.Warn(&sql.Warning{ 405 Level: "Note", 406 Code: 1544, 407 Message: "Event execution time is in the past. Event has been disabled", 408 }) 409 } else { 410 // If ON COMPLETION NOT PRESERVE is defined, the event is dropped immediately after creation. 411 err = c.eventDb.DropEvent(ctx, c.event.Name) 412 if err != nil { 413 return nil, err 414 } 415 ctx.Session.Warn(&sql.Warning{ 416 Level: "Note", 417 Code: 1588, 418 Message: "Event execution time is in the past and ON COMPLETION NOT PRESERVE is set. The event was dropped immediately after creation.", 419 }) 420 } 421 return sql.Row{types.NewOkResult(0)}, nil 422 } 423 } 424 425 // make sure to notify the EventSchedulerStatus AFTER adding the event in the database 426 if c.eventScheduler != nil && enabled { 427 c.eventScheduler.AddEvent(ctx, c.eventDb, c.event) 428 } 429 430 return sql.Row{types.NewOkResult(0)}, nil 431 } 432 433 // Close implements the sql.RowIter interface. 434 func (c *createEventIter) Close(ctx *sql.Context) error { 435 return nil 436 } 437 438 // onScheduleEveryString returns ON SCHEDULE EVERY clause part of CREATE EVENT statement. 439 func onScheduleEveryString(every sql.Expression, starts, ends *OnScheduleTimestamp) string { 440 everyInterval := strings.TrimPrefix(every.String(), "INTERVAL ") 441 startsStr := "" 442 if starts != nil { 443 startsStr = fmt.Sprintf(" %s", starts.String()) 444 } 445 endsStr := "" 446 if ends != nil { 447 endsStr = fmt.Sprintf(" %s", ends.String()) 448 } 449 450 return fmt.Sprintf("ON SCHEDULE EVERY %s%s%s", everyInterval, startsStr, endsStr) 451 } 452 453 // OnScheduleTimestamp is object used for EVENT ON SCHEDULE { AT / STARTS / ENDS } optional fields only. 454 type OnScheduleTimestamp struct { 455 field string 456 timestamp sql.Expression 457 intervals []sql.Expression 458 } 459 460 var _ sql.Expression = (*OnScheduleTimestamp)(nil) 461 462 // NewOnScheduleTimestamp creates OnScheduleTimestamp object used for EVENT ON SCHEDULE { AT / STARTS / ENDS } optional fields only. 463 func NewOnScheduleTimestamp(f string, ts sql.Expression, i []sql.Expression) *OnScheduleTimestamp { 464 return &OnScheduleTimestamp{ 465 field: f, 466 timestamp: ts, 467 intervals: i, 468 } 469 } 470 471 func (ost *OnScheduleTimestamp) IsReadOnly() bool { 472 return true 473 } 474 475 func (ost *OnScheduleTimestamp) Type() sql.Type { 476 return ost.timestamp.Type() 477 } 478 479 func (ost *OnScheduleTimestamp) IsNullable() bool { 480 if ost.timestamp.IsNullable() { 481 return true 482 } 483 for _, i := range ost.intervals { 484 if i.IsNullable() { 485 return true 486 } 487 } 488 return false 489 } 490 491 func (ost *OnScheduleTimestamp) Children() []sql.Expression { 492 var exprs = []sql.Expression{ost.timestamp} 493 return append(exprs, ost.intervals...) 494 } 495 496 func (ost *OnScheduleTimestamp) WithChildren(children ...sql.Expression) (sql.Expression, error) { 497 if len(children) == 0 { 498 return nil, sql.ErrInvalidChildrenNumber.New(ost, len(children), "at least 1") 499 } 500 501 var intervals = make([]sql.Expression, 0) 502 if len(children) > 1 { 503 intervals = append(intervals, children[1:]...) 504 } 505 506 return NewOnScheduleTimestamp(ost.field, children[0], intervals), nil 507 } 508 509 // Resolved implements the sql.Node interface. 510 func (ost *OnScheduleTimestamp) Resolved() bool { 511 var children = []sql.Expression{ost.timestamp} 512 children = append(children, ost.intervals...) 513 for _, child := range children { 514 if !child.Resolved() { 515 return false 516 } 517 } 518 return true 519 } 520 521 // String implements the sql.Node interface. 522 func (ost *OnScheduleTimestamp) String() string { 523 intervals := "" 524 for _, interval := range ost.intervals { 525 intervals = fmt.Sprintf("%s + %s", intervals, interval.String()) 526 } 527 return fmt.Sprintf("%s %s%s", ost.field, ost.timestamp.String(), intervals) 528 } 529 530 func (ost *OnScheduleTimestamp) Eval(ctx *sql.Context, row sql.Row) (interface{}, error) { 531 panic("OnScheduleTimestamp.Eval is just a placeholder method and should not be called directly") 532 } 533 534 // EvalTime returns time.Time value converted to UTC evaluating given expressions as expected to be time value 535 // and optional interval values. The value returned is time.Time value from timestamp value plus all intervals given. 536 func (ost *OnScheduleTimestamp) EvalTime(ctx *sql.Context, tz string) (time.Time, error) { 537 value, err := ost.timestamp.Eval(ctx, nil) 538 if err != nil { 539 return time.Time{}, err 540 } 541 542 if bs, ok := value.([]byte); ok { 543 value = string(bs) 544 } 545 546 var t time.Time 547 switch v := value.(type) { 548 case time.Time: 549 // TODO: check if this value is in session timezone 550 t = v 551 case string: 552 t, err = sql.GetTimeValueFromStringInput(ost.field, v) 553 if err != nil { 554 return time.Time{}, err 555 } 556 default: 557 return time.Time{}, fmt.Errorf("unexpected type: %s", v) 558 } 559 560 for _, interval := range ost.intervals { 561 i, ok := interval.(*expression.Interval) 562 if !ok { 563 return time.Time{}, fmt.Errorf("expected interval but got: %s", interval) 564 } 565 566 timeDelta, err := i.EvalDelta(ctx, nil) 567 if err != nil { 568 return time.Time{}, err 569 } 570 t = timeDelta.Add(t) 571 } 572 573 // truncates the timezone part from the time value and returns the time value in given TZ 574 truncatedVal, err := time.Parse(sql.EventDateSpaceTimeFormat, t.Format(sql.EventDateSpaceTimeFormat)) 575 if err != nil { 576 return time.Time{}, err 577 } 578 return gmstime.ConvertTimeToLocation(truncatedVal, tz) 579 } 580 581 var _ sql.Node = (*DropEvent)(nil) 582 var _ sql.Databaser = (*DropEvent)(nil) 583 var _ sql.EventSchedulerStatement = (*DropEvent)(nil) 584 585 type DropEvent struct { 586 ddlNode 587 EventName string 588 IfExists bool 589 // eventScheduler is used to notify EventSchedulerStatus of the event deletion 590 eventScheduler sql.EventScheduler 591 } 592 593 // NewDropEvent creates a new *DropEvent node. 594 func NewDropEvent(db sql.Database, eventName string, ifExists bool) *DropEvent { 595 return &DropEvent{ 596 ddlNode: ddlNode{db}, 597 EventName: strings.ToLower(eventName), 598 IfExists: ifExists, 599 } 600 } 601 602 // String implements the sql.Node interface. 603 func (d *DropEvent) String() string { 604 ifExists := "" 605 if d.IfExists { 606 ifExists = "IF EXISTS " 607 } 608 return fmt.Sprintf("DROP PROCEDURE %s%s", ifExists, d.EventName) 609 } 610 611 // Schema implements the sql.Node interface. 612 func (d *DropEvent) Schema() sql.Schema { 613 return nil 614 } 615 616 func (d *DropEvent) IsReadOnly() bool { 617 return false 618 } 619 620 // RowIter implements the sql.Node interface. 621 func (d *DropEvent) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) { 622 eventDb, ok := d.Db.(sql.EventDatabase) 623 if !ok { 624 if d.IfExists { 625 return sql.RowsToRowIter(), nil 626 } else { 627 return nil, sql.ErrEventsNotSupported.New(d.EventName) 628 } 629 } 630 631 // make sure to notify the EventSchedulerStatus before dropping the event in the database 632 if d.eventScheduler != nil { 633 d.eventScheduler.RemoveEvent(eventDb.Name(), d.EventName) 634 } 635 636 err := eventDb.DropEvent(ctx, d.EventName) 637 if d.IfExists && sql.ErrEventDoesNotExist.Is(err) && ctx != nil && ctx.Session != nil { 638 ctx.Session.Warn(&sql.Warning{ 639 Level: "Note", 640 Code: 1305, 641 Message: fmt.Sprintf("Event %s does not exist", d.EventName), 642 }) 643 } else if err != nil { 644 return nil, err 645 } 646 647 return sql.RowsToRowIter(sql.Row{types.NewOkResult(0)}), nil 648 } 649 650 // WithChildren implements the sql.Node interface. 651 func (d *DropEvent) WithChildren(children ...sql.Node) (sql.Node, error) { 652 return NillaryWithChildren(d, children...) 653 } 654 655 // CheckPrivileges implements the interface sql.Node. 656 func (d *DropEvent) CheckPrivileges(ctx *sql.Context, opChecker sql.PrivilegedOperationChecker) bool { 657 subject := sql.PrivilegeCheckSubject{ 658 Database: d.Db.Name(), 659 } 660 return opChecker.UserHasPrivileges(ctx, sql.NewPrivilegedOperation(subject, sql.PrivilegeType_Event)) 661 } 662 663 // WithDatabase implements the sql.Databaser interface. 664 func (d *DropEvent) WithDatabase(database sql.Database) (sql.Node, error) { 665 nde := *d 666 nde.Db = database 667 return &nde, nil 668 } 669 670 // WithEventScheduler is used to notify EventSchedulerStatus to update the events list for DROP EVENT. 671 func (d *DropEvent) WithEventScheduler(scheduler sql.EventScheduler) sql.Node { 672 nd := *d 673 nd.eventScheduler = scheduler 674 return &nd 675 }