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(&param.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  }