github.com/dolthub/go-mysql-server@v0.18.0/sql/planbuilder/create_ddl.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 planbuilder 16 17 import ( 18 "fmt" 19 "strings" 20 "time" 21 "unicode" 22 23 ast "github.com/dolthub/vitess/go/vt/sqlparser" 24 25 "github.com/dolthub/go-mysql-server/sql" 26 "github.com/dolthub/go-mysql-server/sql/expression" 27 "github.com/dolthub/go-mysql-server/sql/plan" 28 "github.com/dolthub/go-mysql-server/sql/types" 29 ) 30 31 func (b *Builder) buildCreateTrigger(inScope *scope, query string, c *ast.DDL) (outScope *scope) { 32 outScope = inScope.push() 33 var triggerOrder *plan.TriggerOrder 34 if c.TriggerSpec.Order != nil { 35 triggerOrder = &plan.TriggerOrder{ 36 PrecedesOrFollows: c.TriggerSpec.Order.PrecedesOrFollows, 37 OtherTriggerName: c.TriggerSpec.Order.OtherTriggerName, 38 } 39 } else { 40 //TODO: fix vitess->sql.y, in CREATE TRIGGER, if trigger_order_opt evaluates to empty then SubStatementPositionStart swallows the first token of the body 41 beforeSwallowedToken := strings.LastIndexFunc(strings.TrimRightFunc(query[:c.SubStatementPositionStart], unicode.IsSpace), unicode.IsSpace) 42 if beforeSwallowedToken != -1 { 43 c.SubStatementPositionStart = beforeSwallowedToken 44 } 45 } 46 47 // resolve table -> create initial scope 48 dbName := c.Table.Qualifier.String() 49 if dbName == "" { 50 dbName = b.ctx.GetCurrentDatabase() 51 } 52 53 prevTriggerCtxActive := b.TriggerCtx().Active 54 b.TriggerCtx().Active = true 55 defer func() { 56 b.TriggerCtx().Active = prevTriggerCtxActive 57 }() 58 59 tableName := strings.ToLower(c.Table.Name.String()) 60 tableScope, ok := b.buildResolvedTable(inScope, dbName, tableName, nil) 61 if !ok { 62 b.handleErr(sql.ErrTableNotFound.New(tableName)) 63 } 64 if _, ok := tableScope.node.(*plan.UnresolvedTable); ok { 65 // unknown table in trigger body is OK, but the target table must exist 66 b.handleErr(sql.ErrTableNotFound.New(tableName)) 67 } 68 69 // todo scope with new and old columns provided 70 // insert/update have "new" 71 // update/delete have "old" 72 newScope := tableScope.replace() 73 oldScope := tableScope.replace() 74 for _, col := range tableScope.cols { 75 switch c.TriggerSpec.Event { 76 case ast.InsertStr: 77 newScope.newColumn(col) 78 case ast.UpdateStr: 79 newScope.newColumn(col) 80 oldScope.newColumn(col) 81 case ast.DeleteStr: 82 oldScope.newColumn(col) 83 } 84 } 85 newScope.setTableAlias("new") 86 oldScope.setTableAlias("old") 87 triggerScope := tableScope.replace() 88 89 triggerScope.addColumns(newScope.cols) 90 triggerScope.addColumns(oldScope.cols) 91 92 bodyStr := strings.TrimSpace(query[c.SubStatementPositionStart:c.SubStatementPositionEnd]) 93 bodyScope := b.build(triggerScope, c.TriggerSpec.Body, bodyStr) 94 definer := getCurrentUserForDefiner(b.ctx, c.TriggerSpec.Definer) 95 db := b.resolveDb(dbName) 96 97 if _, ok := tableScope.node.(*plan.ResolvedTable); !ok { 98 if prevTriggerCtxActive { 99 // previous ctx set means this is an INSERT or SHOW 100 // old version of Dolt permitted a bad trigger on VIEW 101 // warn and noop 102 b.ctx.Warn(0, fmt.Sprintf("trigger on view is not supported; 'DROP TRIGGER %s' to fix", c.TriggerSpec.TrigName.Name.String())) 103 bodyScope.node = plan.NewResolvedDualTable() 104 } else { 105 // top-level call is DDL 106 err := sql.ErrExpectedTableFoundView.New(tableName) 107 b.handleErr(err) 108 } 109 } 110 111 outScope.node = plan.NewCreateTrigger( 112 db, 113 c.TriggerSpec.TrigName.Name.String(), 114 c.TriggerSpec.Time, 115 c.TriggerSpec.Event, 116 triggerOrder, 117 tableScope.node, 118 bodyScope.node, 119 query, 120 bodyStr, 121 b.ctx.QueryTime(), 122 definer, 123 ) 124 return outScope 125 } 126 127 func getCurrentUserForDefiner(ctx *sql.Context, definer string) string { 128 if definer == "" { 129 client := ctx.Session.Client() 130 definer = fmt.Sprintf("`%s`@`%s`", client.User, client.Address) 131 } 132 return definer 133 } 134 135 func (b *Builder) buildCreateProcedure(inScope *scope, query string, c *ast.DDL) (outScope *scope) { 136 var params []plan.ProcedureParam 137 for _, param := range c.ProcedureSpec.Params { 138 var direction plan.ProcedureParamDirection 139 switch param.Direction { 140 case ast.ProcedureParamDirection_In: 141 direction = plan.ProcedureParamDirection_In 142 case ast.ProcedureParamDirection_Inout: 143 direction = plan.ProcedureParamDirection_Inout 144 case ast.ProcedureParamDirection_Out: 145 direction = plan.ProcedureParamDirection_Out 146 default: 147 err := fmt.Errorf("unknown procedure parameter direction: `%s`", string(param.Direction)) 148 b.handleErr(err) 149 } 150 internalTyp, err := types.ColumnTypeToType(¶m.Type) 151 if err != nil { 152 b.handleErr(err) 153 } 154 params = append(params, plan.ProcedureParam{ 155 Direction: direction, 156 Name: param.Name, 157 Type: internalTyp, 158 Variadic: false, 159 }) 160 } 161 162 var characteristics []plan.Characteristic 163 securityType := plan.ProcedureSecurityContext_Definer // Default Security Context 164 comment := "" 165 for _, characteristic := range c.ProcedureSpec.Characteristics { 166 switch characteristic.Type { 167 case ast.CharacteristicValue_Comment: 168 comment = characteristic.Comment 169 case ast.CharacteristicValue_LanguageSql: 170 characteristics = append(characteristics, plan.Characteristic_LanguageSql) 171 case ast.CharacteristicValue_Deterministic: 172 characteristics = append(characteristics, plan.Characteristic_Deterministic) 173 case ast.CharacteristicValue_NotDeterministic: 174 characteristics = append(characteristics, plan.Characteristic_NotDeterministic) 175 case ast.CharacteristicValue_ContainsSql: 176 characteristics = append(characteristics, plan.Characteristic_ContainsSql) 177 case ast.CharacteristicValue_NoSql: 178 characteristics = append(characteristics, plan.Characteristic_NoSql) 179 case ast.CharacteristicValue_ReadsSqlData: 180 characteristics = append(characteristics, plan.Characteristic_ReadsSqlData) 181 case ast.CharacteristicValue_ModifiesSqlData: 182 characteristics = append(characteristics, plan.Characteristic_ModifiesSqlData) 183 case ast.CharacteristicValue_SqlSecurityDefiner: 184 // This is already the default value, so this prevents the default switch case 185 case ast.CharacteristicValue_SqlSecurityInvoker: 186 securityType = plan.ProcedureSecurityContext_Invoker 187 default: 188 err := fmt.Errorf("unknown procedure characteristic: `%s`", string(characteristic.Type)) 189 b.handleErr(err) 190 } 191 } 192 193 inScope.initProc() 194 procName := strings.ToLower(c.ProcedureSpec.ProcName.Name.String()) 195 for _, p := range params { 196 // populate inScope with the procedure parameters. this will be 197 // subject maybe a bug where an inner procedure has access to 198 // outer procedure parameters. 199 inScope.proc.AddVar(expression.NewProcedureParam(strings.ToLower(p.Name), p.Type)) 200 } 201 bodyStr := strings.TrimSpace(query[c.SubStatementPositionStart:c.SubStatementPositionEnd]) 202 203 bodyScope := b.build(inScope, c.ProcedureSpec.Body, bodyStr) 204 205 var db sql.Database = nil 206 dbName := c.ProcedureSpec.ProcName.Qualifier.String() 207 if dbName != "" { 208 db = b.resolveDb(dbName) 209 } else { 210 db = b.currentDb() 211 } 212 213 outScope = inScope.push() 214 outScope.node = plan.NewCreateProcedure( 215 db, 216 procName, 217 c.ProcedureSpec.Definer, 218 params, 219 time.Now(), 220 time.Now(), 221 securityType, 222 characteristics, 223 bodyScope.node, 224 comment, 225 query, 226 bodyStr, 227 ) 228 return outScope 229 } 230 231 func (b *Builder) buildCreateEvent(inScope *scope, query string, c *ast.DDL) (outScope *scope) { 232 outScope = inScope.push() 233 eventSpec := c.EventSpec 234 dbName := strings.ToLower(eventSpec.EventName.Qualifier.String()) 235 if dbName == "" { 236 dbName = b.ctx.GetCurrentDatabase() 237 } 238 database := b.resolveDb(dbName) 239 definer := getCurrentUserForDefiner(b.ctx, c.EventSpec.Definer) 240 241 // both 'undefined' and 'not preserve' are considered 'not preserve' 242 onCompletionPreserve := false 243 if eventSpec.OnCompletionPreserve == ast.EventOnCompletion_Preserve { 244 onCompletionPreserve = true 245 } 246 247 var status sql.EventStatus 248 switch eventSpec.Status { 249 case ast.EventStatus_Undefined: 250 status = sql.EventStatus_Enable 251 case ast.EventStatus_Enable: 252 status = sql.EventStatus_Enable 253 case ast.EventStatus_Disable: 254 status = sql.EventStatus_Disable 255 case ast.EventStatus_DisableOnSlave: 256 status = sql.EventStatus_DisableOnSlave 257 } 258 259 bodyStr := strings.TrimSpace(query[c.SubStatementPositionStart:c.SubStatementPositionEnd]) 260 bodyScope := b.build(inScope, c.EventSpec.Body, bodyStr) 261 262 var at, starts, ends *plan.OnScheduleTimestamp 263 var everyInterval *expression.Interval 264 if eventSpec.OnSchedule.At != nil { 265 ts, intervals := b.buildEventScheduleTimeSpec(inScope, eventSpec.OnSchedule.At) 266 at = plan.NewOnScheduleTimestamp("AT", ts, intervals) 267 } else { 268 everyInterval = b.intervalExprToExpression(inScope, &eventSpec.OnSchedule.EveryInterval) 269 if eventSpec.OnSchedule.Starts != nil { 270 startsTs, startsIntervals := b.buildEventScheduleTimeSpec(inScope, eventSpec.OnSchedule.Starts) 271 starts = plan.NewOnScheduleTimestamp("STARTS", startsTs, startsIntervals) 272 } 273 if eventSpec.OnSchedule.Ends != nil { 274 endsTs, endsIntervals := b.buildEventScheduleTimeSpec(inScope, eventSpec.OnSchedule.Ends) 275 ends = plan.NewOnScheduleTimestamp("ENDS", endsTs, endsIntervals) 276 } 277 } 278 279 comment := "" 280 if eventSpec.Comment != nil { 281 comment = string(eventSpec.Comment.Val) 282 } 283 284 outScope.node = plan.NewCreateEvent( 285 database, 286 eventSpec.EventName.Name.String(), definer, 287 at, starts, ends, everyInterval, 288 onCompletionPreserve, 289 status, comment, bodyStr, bodyScope.node, eventSpec.IfNotExists, 290 ) 291 return outScope 292 } 293 294 func (b *Builder) buildEventScheduleTimeSpec(inScope *scope, spec *ast.EventScheduleTimeSpec) (sql.Expression, []sql.Expression) { 295 ts := b.buildScalar(inScope, spec.EventTimestamp) 296 if len(spec.EventIntervals) == 0 { 297 return ts, nil 298 } 299 var intervals = make([]sql.Expression, len(spec.EventIntervals)) 300 for i, interval := range spec.EventIntervals { 301 e := b.intervalExprToExpression(inScope, &interval) 302 intervals[i] = e 303 } 304 return ts, intervals 305 } 306 307 func (b *Builder) buildAlterUser(inScope *scope, _ string, c *ast.DDL) (outScope *scope) { 308 database := b.resolveDb("mysql") 309 accountWithAuth := ast.AccountWithAuth{AccountName: c.User, Auth1: c.Authentication} 310 user := b.buildAuthenticatedUser(accountWithAuth) 311 312 if c.Authentication.RandomPassword { 313 b.handleErr(fmt.Errorf("random password generation is not currently supported; " + 314 "you can request support at https://github.com/dolthub/dolt/issues/new")) 315 } 316 317 outScope = inScope.push() 318 outScope.node = &plan.AlterUser{ 319 IfExists: c.IfExists, 320 User: user, 321 MySQLDb: database, 322 } 323 return outScope 324 } 325 326 func (b *Builder) buildAlterEvent(inScope *scope, query string, c *ast.DDL) (outScope *scope) { 327 eventSpec := c.EventSpec 328 329 var database sql.Database 330 if dbName := eventSpec.EventName.Qualifier.String(); dbName != "" { 331 database = b.resolveDb(dbName) 332 } else { 333 database = b.currentDb() 334 } 335 336 definer := getCurrentUserForDefiner(b.ctx, c.EventSpec.Definer) 337 338 var ( 339 alterSchedule = eventSpec.OnSchedule != nil 340 at, starts, ends *plan.OnScheduleTimestamp 341 everyInterval *expression.Interval 342 343 alterOnComp = eventSpec.OnCompletionPreserve != ast.EventOnCompletion_Undefined 344 newOnCompPreserve = eventSpec.OnCompletionPreserve == ast.EventOnCompletion_Preserve 345 346 alterEventName = !eventSpec.RenameName.IsEmpty() 347 newName string 348 349 alterStatus = eventSpec.Status != ast.EventStatus_Undefined 350 newStatus sql.EventStatus 351 352 alterComment = eventSpec.Comment != nil 353 newComment string 354 355 alterDefinition = eventSpec.Body != nil 356 newDefinitionStr string 357 newDefinition sql.Node 358 ) 359 360 if alterSchedule { 361 if eventSpec.OnSchedule.At != nil { 362 ts, intervals := b.buildEventScheduleTimeSpec(inScope, eventSpec.OnSchedule.At) 363 at = plan.NewOnScheduleTimestamp("AT", ts, intervals) 364 } else { 365 everyInterval = b.intervalExprToExpression(inScope, &eventSpec.OnSchedule.EveryInterval) 366 if eventSpec.OnSchedule.Starts != nil { 367 startsTs, startsIntervals := b.buildEventScheduleTimeSpec(inScope, eventSpec.OnSchedule.Starts) 368 starts = plan.NewOnScheduleTimestamp("STARTS", startsTs, startsIntervals) 369 } 370 if eventSpec.OnSchedule.Ends != nil { 371 endsTs, endsIntervals := b.buildEventScheduleTimeSpec(inScope, eventSpec.OnSchedule.Ends) 372 ends = plan.NewOnScheduleTimestamp("ENDS", endsTs, endsIntervals) 373 } 374 } 375 } 376 if alterEventName { 377 // events can be moved to different database using RENAME TO clause option 378 // TODO: we do not support moving events to different database yet 379 renameEventDb := eventSpec.RenameName.Qualifier.String() 380 if renameEventDb != "" && database.Name() != renameEventDb { 381 err := fmt.Errorf("moving events to different database using ALTER EVENT is not supported yet") 382 b.handleErr(err) 383 } 384 newName = eventSpec.RenameName.Name.String() 385 } 386 if alterStatus { 387 switch eventSpec.Status { 388 case ast.EventStatus_Undefined: 389 // this should not happen but sanity check 390 newStatus = sql.EventStatus_Enable 391 case ast.EventStatus_Enable: 392 newStatus = sql.EventStatus_Enable 393 case ast.EventStatus_Disable: 394 newStatus = sql.EventStatus_Disable 395 case ast.EventStatus_DisableOnSlave: 396 newStatus = sql.EventStatus_DisableOnSlave 397 } 398 } 399 if alterComment { 400 newComment = string(eventSpec.Comment.Val) 401 } 402 if alterDefinition { 403 newDefinitionStr = strings.TrimSpace(query[c.SubStatementPositionStart:c.SubStatementPositionEnd]) 404 defScope := b.build(inScope, c.EventSpec.Body, newDefinitionStr) 405 newDefinition = defScope.node 406 } 407 408 eventName := strings.ToLower(eventSpec.EventName.Name.String()) 409 eventDb, ok := database.(sql.EventDatabase) 410 if !ok { 411 err := sql.ErrEventsNotSupported.New(database.Name()) 412 b.handleErr(err) 413 } 414 415 event, exists, err := eventDb.GetEvent(b.ctx, eventName) 416 if err != nil { 417 b.handleErr(err) 418 } 419 if !exists { 420 err := sql.ErrEventDoesNotExist.New(eventName) 421 b.handleErr(err) 422 } 423 424 outScope = inScope.push() 425 alterEvent := plan.NewAlterEvent( 426 database, eventName, definer, 427 alterSchedule, at, starts, ends, everyInterval, 428 alterOnComp, newOnCompPreserve, 429 alterEventName, newName, 430 alterStatus, newStatus, 431 alterComment, newComment, 432 alterDefinition, newDefinitionStr, newDefinition, 433 ) 434 alterEvent.Event = event 435 outScope.node = alterEvent 436 return 437 } 438 439 func (b *Builder) buildCreateView(inScope *scope, query string, c *ast.DDL) (outScope *scope) { 440 outScope = inScope.push() 441 442 selectStr := query[c.SubStatementPositionStart:c.SubStatementPositionEnd] 443 stmt, _, err := ast.ParseOneWithOptions(selectStr, b.parserOpts) 444 if err != nil { 445 b.handleErr(err) 446 } 447 selectStatement, ok := stmt.(ast.SelectStatement) 448 if !ok { 449 err := sql.ErrUnsupportedSyntax.New(ast.String(c.ViewSpec.ViewExpr)) 450 b.handleErr(err) 451 } 452 queryScope := b.buildSelectStmt(inScope, selectStatement) 453 454 queryAlias := plan.NewSubqueryAlias(c.ViewSpec.ViewName.Name.String(), selectStr, queryScope.node) 455 definer := getCurrentUserForDefiner(b.ctx, c.ViewSpec.Definer) 456 457 if len(c.ViewSpec.Columns) > 0 { 458 if len(c.ViewSpec.Columns) != len(queryScope.cols) { 459 err := sql.ErrInvalidColumnNumber.New(len(queryScope.cols), len(c.ViewSpec.Columns)) 460 b.handleErr(err) 461 } 462 queryAlias = queryAlias.WithColumnNames(columnsToStrings(c.ViewSpec.Columns)) 463 } 464 465 dbName := c.ViewSpec.ViewName.Qualifier.String() 466 if dbName == "" { 467 dbName = b.ctx.GetCurrentDatabase() 468 } 469 db := b.resolveDb(dbName) 470 outScope.node = plan.NewCreateView(db, c.ViewSpec.ViewName.Name.String(), queryAlias, c.OrReplace, query, c.ViewSpec.Algorithm, definer, c.ViewSpec.Security) 471 return outScope 472 }