Repository: vitessio/vitess
Pull request #7045 (2020-11-16 14:08:26)

Online DDL: ddl_strategy session variable and vtctl command line argument
Context: #6782 (comment)
Additional contexts: #6926, #6689
Extends #7042 

In this PR we utilize the new `ddl_strategy` session variable. Consider the following:

```sql

mysql> show create table t \G

Create Table: CREATE TABLE `t` (
  `id` smallint(6) NOT NULL,
  `i` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

mysql> select @@ddl_strategy;
+----------------+
| @@ddl_strategy |
+----------------+
|                |
+----------------+

mysql> alter table t modify id bigint;
Query OK, 0 rows affected (0.03 sec)

mysql> show create table t \G

Create Table: CREATE TABLE `t` (
  `id` bigint(20) NOT NULL,
  `i` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

mysql> set @@ddl_strategy='gh-ost';
Query OK, 0 rows affected (0.00 sec)

mysql> alter table t modify id mediumint;
+--------------------------------------+
| uuid                                 |
+--------------------------------------+
| b87f9824_2814_11eb_bd3d_f875a4d24e90 |
+--------------------------------------+

mysql> show create table t \G

Create Table: CREATE TABLE `t` (
  `id` bigint(20) NOT NULL,
  `i` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

mysql> -- time passes

mysql> show create table t \G

Create Table: CREATE TABLE `t` (
  `id` mediumint(9) NOT NULL,
  `i` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
```

Thus, the choice whether to user "normal" DDL or Online DDL, can depend on the valud of `@@ddl_strategy` session variable, which is either:

- empty (normal DD)
- `gh-ost`
- `pt-osc`

I'm not sure yet how to represent migration options. Would that be `set @@ddl_strategy='gh-ost; max-load=Threads_running=100'`? Seems quite the syntax.



Patch:
diff --git a/go/vt/schema/online_ddl.go b/go/vt/schema/online_ddl.go
index 644a738254..86ed960298 100644
--- a/go/vt/schema/online_ddl.go
+++ b/go/vt/schema/online_ddl.go
@@ -31,8 +31,9 @@ import (
 )
 
 var (
-	migrationBasePath   = "schema-migration"
-	onlineDdlUUIDRegexp = regexp.MustCompile(`^[0-f]{8}_[0-f]{4}_[0-f]{4}_[0-f]{4}_[0-f]{12}$`)
+	migrationBasePath    = "schema-migration"
+	onlineDdlUUIDRegexp  = regexp.MustCompile(`^[0-f]{8}_[0-f]{4}_[0-f]{4}_[0-f]{4}_[0-f]{12}$`)
+	strategyParserRegexp = regexp.MustCompile(`^([\S]+)\s+(.*)$`)
 )
 
 // MigrationBasePath is the root for all schema migration entries
@@ -99,16 +100,19 @@ type OnlineDDL struct {
 	Retries     int64                 `json:"retries,omitempty"`
 }
 
-func ValidateDDLStrategy(strategy string) (sqlparser.DDLStrategy, error) {
-	switch sqlparser.DDLStrategy(strategy) {
-	case DDLStrategyGhost:
-		return DDLStrategyGhost, nil
-	case DDLStrategyPTOSC:
-		return DDLStrategyPTOSC, nil
-	case DDLStrategyNormal:
-		return DDLStrategyNormal, nil
+// ParseDDLStrategy validates the given ddl_strategy variable value , and parses the strategy and options parts.
+func ParseDDLStrategy(strategyVariable string) (strategy sqlparser.DDLStrategy, options string, err error) {
+	strategyName := strategyVariable
+	if submatch := strategyParserRegexp.FindStringSubmatch(strategyVariable); len(submatch) > 0 {
+		strategyName = submatch[1]
+		options = submatch[2]
+	}
+
+	switch strategy = sqlparser.DDLStrategy(strategyName); strategy {
+	case DDLStrategyGhost, DDLStrategyPTOSC, DDLStrategyNormal:
+		return strategy, options, nil
 	default:
-		return DDLStrategyNormal, fmt.Errorf("Unknown online DDL strategy: '%v'", strategy)
+		return strategy, options, fmt.Errorf("Unknown online DDL strategy: '%v'", strategy)
 	}
 }
 
diff --git a/go/vt/schemamanager/tablet_executor.go b/go/vt/schemamanager/tablet_executor.go
index 1f933967fa..d71a8eed92 100644
--- a/go/vt/schemamanager/tablet_executor.go
+++ b/go/vt/schemamanager/tablet_executor.go
@@ -40,6 +40,7 @@ type TabletExecutor struct {
 	allowBigSchemaChange bool
 	keyspace             string
 	waitReplicasTimeout  time.Duration
+	ddlStrategy          string
 }
 
 // NewTabletExecutor creates a new TabletExecutor instance
@@ -64,6 +65,15 @@ func (exec *TabletExecutor) DisallowBigSchemaChange() {
 	exec.allowBigSchemaChange = false
 }
 
+// SetDDLStrategy applies ddl_strategy from command line flags
+func (exec *TabletExecutor) SetDDLStrategy(ddlStrategy string) error {
+	if _, _, err := schema.ParseDDLStrategy(ddlStrategy); err != nil {
+		return err
+	}
+	exec.ddlStrategy = ddlStrategy
+	return nil
+}
+
 // Open opens a connection to the master for every shard.
 func (exec *TabletExecutor) Open(ctx context.Context, keyspace string) error {
 	if !exec.isClosed {
@@ -140,6 +150,21 @@ func (exec *TabletExecutor) parseDDLs(sqls []string) ([]*sqlparser.DDL, []*sqlpa
 	return parsedDDLs, parsedDBDDLs, nil
 }
 
+// IsOnlineSchemaDDL returns true if the query is an online schema change DDL
+func (exec *TabletExecutor) isOnlineSchemaDDL(ddl *sqlparser.DDL) (isOnline bool, strategy sqlparser.DDLStrategy, options string) {
+	if ddl.Action != sqlparser.AlterDDLAction {
+		return false, strategy, options
+	}
+	strategy, options, _ = schema.ParseDDLStrategy(exec.ddlStrategy)
+	if strategy != schema.DDLStrategyNormal {
+		return true, strategy, options
+	}
+	if ddl.OnlineHint != nil {
+		return ddl.OnlineHint.Strategy != schema.DDLStrategyNormal, ddl.OnlineHint.Strategy, ddl.OnlineHint.Options
+	}
+	return false, strategy, options
+}
+
 // a schema change that satisfies any following condition is considered
 // to be a big schema change and will be rejected.
 //   1. Alter more than 100,000 rows.
@@ -159,16 +184,13 @@ func (exec *TabletExecutor) detectBigSchemaChanges(ctx context.Context, parsedDD
 		tableWithCount[tableSchema.Name] = tableSchema.RowCount
 	}
 	for _, ddl := range parsedDDLs {
+		if isOnline, _, _ := exec.isOnlineSchemaDDL(ddl); isOnline {
+			// Since this is an online schema change, there is no need to worry about big changes
+			continue
+		}
 		switch ddl.Action {
 		case sqlparser.DropDDLAction, sqlparser.CreateDDLAction, sqlparser.TruncateDDLAction, sqlparser.RenameDDLAction:
 			continue
-		case sqlparser.AlterDDLAction:
-			if ddl.OnlineHint != nil {
-				if ddl.OnlineHint.Strategy != schema.DDLStrategyNormal {
-					// Seeing that we intend to run an online-schema-change, we can skip the "big change" check.
-					continue
-				}
-			}
 		}
 		tableName := ddl.Table.Name.String()
 		if rowCount, ok := tableWithCount[tableName]; ok {
@@ -248,13 +270,8 @@ func (exec *TabletExecutor) Execute(ctx context.Context, sqls []string) *Execute
 		tableName := ""
 		switch ddl := stat.(type) {
 		case *sqlparser.DDL:
-			if ddl.Action == sqlparser.AlterDDLAction {
-				if ddl.OnlineHint != nil {
-					strategy = ddl.OnlineHint.Strategy
-					options = ddl.OnlineHint.Options
-				}
-			}
 			tableName = ddl.Table.Name.String()
+			_, strategy, options = exec.isOnlineSchemaDDL(ddl)
 		}
 		exec.wr.Logger().Infof("Received DDL request. strategy = %+v", strategy)
 		exec.executeOnAllTablets(ctx, &execResult, sql, tableName, strategy, options)
diff --git a/go/vt/sqlparser/analyzer.go b/go/vt/sqlparser/analyzer.go
index 6d3848793a..cd65b44366 100644
--- a/go/vt/sqlparser/analyzer.go
+++ b/go/vt/sqlparser/analyzer.go
@@ -267,14 +267,6 @@ func IsVschemaDDL(ddl DDLStatement) bool {
 	return false
 }
 
-// IsOnlineSchemaDDL returns true if the query is an online schema change DDL
-func IsOnlineSchemaDDL(ddl DDLStatement) bool {
-	if ddl.GetOnlineHint() != nil {
-		return ddl.GetOnlineHint().Strategy != ""
-	}
-	return false
-}
-
 // SplitAndExpression breaks up the Expr into AND-separated conditions
 // and appends them to filters. Outer parenthesis are removed. Precedence
 // should be taken into account if expressions are recombined.
diff --git a/go/vt/sqlparser/ast.go b/go/vt/sqlparser/ast.go
index 472eb86624..cbe977dbb2 100644
--- a/go/vt/sqlparser/ast.go
+++ b/go/vt/sqlparser/ast.go
@@ -58,8 +58,10 @@ type (
 	DDLStatement interface {
 		iDDLStatement()
 		GetOnlineHint() *OnlineDDLHint
+		SetOnlineHint(*OnlineDDLHint)
 		IsFullyParsed() bool
 		GetTable() TableName
+		GetAction() DDLAction
 		AffectedTables() TableNames
 		SetTable(qualifier string, name string)
 		Statement
@@ -390,6 +392,16 @@ func (node *CreateIndex) GetOnlineHint() *OnlineDDLHint {
 	return node.OnlineHint
 }
 
+// SetOnlineHint implements the DDLStatement interface
+func (node *DDL) SetOnlineHint(hint *OnlineDDLHint) {
+	node.OnlineHint = hint
+}
+
+// SetOnlineHint implements the DDLStatement interface
+func (node *CreateIndex) SetOnlineHint(hint *OnlineDDLHint) {
+	node.OnlineHint = hint
+}
+
 // GetTable implements the DDLStatement interface
 func (node *CreateIndex) GetTable() TableName {
 	return node.Table
@@ -400,6 +412,16 @@ func (node *DDL) GetTable() TableName {
 	return node.Table
 }
 
+// GetAction implements the DDLStatement interface
+func (node *CreateIndex) GetAction() DDLAction {
+	return AlterDDLAction
+}
+
+// GetAction implements the DDLStatement interface
+func (node *DDL) GetAction() DDLAction {
+	return node.Action
+}
+
 // AffectedTables returns the list table names affected by the DDLStatement.
 func (node *DDL) AffectedTables() TableNames {
 	if node.Action == RenameDDLAction || node.Action == DropDDLAction {
diff --git a/go/vt/vtctl/vtctl.go b/go/vt/vtctl/vtctl.go
index 03ff3476d3..bd86255805 100644
--- a/go/vt/vtctl/vtctl.go
+++ b/go/vt/vtctl/vtctl.go
@@ -406,8 +406,8 @@ var commands = []commandGroup{
 				"[-exclude_tables=''] [-include-views] [-skip-no-master] <keyspace name>",
 				"Validates that the master schema from shard 0 matches the schema on all of the other tablets in the keyspace."},
 			{"ApplySchema", commandApplySchema,
-				"[-allow_long_unavailability] [-wait_replicas_timeout=10s] {-sql=<sql> || -sql-file=<filename>} <keyspace>",
-				"Applies the schema change to the specified keyspace on every master, running in parallel on all shards. The changes are then propagated to replicas via replication. If -allow_long_unavailability is set, schema changes affecting a large number of rows (and possibly incurring a longer period of unavailability) will not be rejected."},
+				"[-allow_long_unavailability] [-wait_replicas_timeout=10s] [-ddl_strategy=<ddl_strategy>] {-sql=<sql> || -sql-file=<filename>} <keyspace>",
+				"Applies the schema change to the specified keyspace on every master, running in parallel on all shards. The changes are then propagated to replicas via replication. If -allow_long_unavailability is set, schema changes affecting a large number of rows (and possibly incurring a longer period of unavailability) will not be rejected. ddl_strategy is used to intruct migrations via gh-ost or pt-osc with optional parameters"},
 			{"CopySchemaShard", commandCopySchemaShard,
 				"[-tables=<table1>,<table2>,...] [-exclude_tables=<table1>,<table2>,...] [-include-views] [-skip-verify] [-wait_replicas_timeout=10s] {<source keyspace/shard> || <source tablet alias>} <destination keyspace/shard>",
 				"Copies the schema from a source shard's master (or a specific tablet) to a destination shard. The schema is applied directly on the master of the destination shard, and it is propagated to the replicas through binlogs."},
@@ -2435,6 +2435,7 @@ func commandApplySchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *fl
 	allowLongUnavailability := subFlags.Bool("allow_long_unavailability", false, "Allow large schema changes which incur a longer unavailability of the database.")
 	sql := subFlags.String("sql", "", "A list of semicolon-delimited SQL commands")
 	sqlFile := subFlags.String("sql-file", "", "Identifies the file that contains the SQL commands")
+	ddlStrategy := subFlags.String("ddl_strategy", "", "Online DDL strategy, compatible with @@ddl_strategy session variable (examples: 'gh-ost', 'pt-osc', 'gh-ost --max-load=Threads_running=100'")
 	// for backwards compatibility
 	deprecatedTimeout := subFlags.Duration("wait_slave_timeout", wrangler.DefaultWaitReplicasTimeout, "DEPRECATED -- use -wait_replicas_timeout")
 	waitReplicasTimeout := subFlags.Duration("wait_replicas_timeout", wrangler.DefaultWaitReplicasTimeout, "The amount of time to wait for replicas to receive the schema change via replication.")
@@ -2458,6 +2459,10 @@ func commandApplySchema(ctx context.Context, wr *wrangler.Wrangler, subFlags *fl
 	if *allowLongUnavailability {
 		executor.AllowBigSchemaChange()
 	}
+	if err := executor.SetDDLStrategy(*ddlStrategy); err != nil {
+		return nil
+	}
+
 	return schemamanager.Run(
 		ctx,
 		schemamanager.NewPlainController(change, keyspace),
diff --git a/go/vt/vtgate/engine/ddl.go b/go/vt/vtgate/engine/ddl.go
new file mode 100644
index 0000000000..10df369b25
--- /dev/null
+++ b/go/vt/vtgate/engine/ddl.go
@@ -0,0 +1,114 @@
+/*
+Copyright 2020 The Vitess Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package engine
+
+import (
+	"vitess.io/vitess/go/sqltypes"
+	"vitess.io/vitess/go/vt/proto/query"
+	vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc"
+	"vitess.io/vitess/go/vt/schema"
+	"vitess.io/vitess/go/vt/sqlparser"
+	"vitess.io/vitess/go/vt/vterrors"
+	"vitess.io/vitess/go/vt/vtgate/vindexes"
+)
+
+var _ Primitive = (*DDL)(nil)
+
+// DDL represents a DDL statement, either normal or online DDL
+type DDL struct {
+	Keyspace *vindexes.Keyspace
+	SQL      string
+	DDL      sqlparser.DDLStatement
+
+	NormalDDL *Send
+	OnlineDDL *OnlineDDL
+
+	noTxNeeded
+
+	noInputs
+}
+
+func (v *DDL) description() PrimitiveDescription {
+	return PrimitiveDescription{
+		OperatorType: "DDL",
+		Keyspace:     v.Keyspace,
+		Other: map[string]interface{}{
+			"Query": v.SQL,
+		},
+	}
+}
+
+// RouteType implements the Primitive interface
+func (v *DDL) RouteType() string {
+	return "DDL"
+}
+
+// GetKeyspaceName implements the Primitive interface
+func (v *DDL) GetKeyspaceName() string {
+	return v.Keyspace.Name
+}
+
+// GetTableName implements the Primitive interface
+func (v *DDL) GetTableName() string {
+	return v.DDL.GetTable().Name.String()
+}
+
+// IsOnlineSchemaDDL returns true if the query is an online schema change DDL
+func (v *DDL) isOnlineSchemaDDL() bool {
+	switch v.DDL.GetAction() {
+	case sqlparser.AlterDDLAction:
+		if v.DDL.GetOnlineHint() != nil {
+			return v.DDL.GetOnlineHint().Strategy != ""
+		}
+	}
+	return false
+}
+
+// Execute implements the Primitive interface
+func (v *DDL) Execute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantfields bool) (result *sqltypes.Result, err error) {
+	if !v.isOnlineSchemaDDL() {
+		strategy, options, err := schema.ParseDDLStrategy(vcursor.Session().GetDDLStrategy())
+		if err != nil {
+			return nil, err
+		}
+		v.OnlineDDL.Strategy = strategy
+		v.OnlineDDL.Options = options
+		v.OnlineDDL.DDL.SetOnlineHint(&sqlparser.OnlineDDLHint{
+			Strategy: strategy,
+			Options:  options,
+		})
+	}
+	if v.isOnlineSchemaDDL() {
+		return v.OnlineDDL.Execute(vcursor, bindVars, wantfields)
+	}
+
+	return v.NormalDDL.Execute(vcursor, bindVars, wantfields)
+}
+
+// StreamExecute implements the Primitive interface
+func (v *DDL) StreamExecute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error {
+	results, err := v.Execute(vcursor, bindVars, wantfields)
+	if err != nil {
+		return err
+	}
+	return callback(results)
+}
+
+// GetFields implements the Primitive interface
+func (v *DDL) GetFields(vcursor VCursor, bindVars map[string]*query.BindVariable) (*sqltypes.Result, error) {
+	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable")
+}
diff --git a/go/vt/vtgate/engine/online_ddl.go b/go/vt/vtgate/engine/online_ddl.go
index d8a0dce103..46dc47f9d1 100644
--- a/go/vt/vtgate/engine/online_ddl.go
+++ b/go/vt/vtgate/engine/online_ddl.go
@@ -96,11 +96,15 @@ func (v *OnlineDDL) Execute(vcursor VCursor, bindVars map[string]*query.BindVari
 }
 
 //StreamExecute implements the Primitive interface
-func (v *OnlineDDL) StreamExecute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantields bool, callback func(*sqltypes.Result) error) error {
-	return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable") // TODO: shlomi - have no idea if this should work
+func (v *OnlineDDL) StreamExecute(vcursor VCursor, bindVars map[string]*query.BindVariable, wantfields bool, callback func(*sqltypes.Result) error) error {
+	results, err := v.Execute(vcursor, bindVars, wantfields)
+	if err != nil {
+		return err
+	}
+	return callback(results)
 }
 
 //GetFields implements the Primitive interface
 func (v *OnlineDDL) GetFields(vcursor VCursor, bindVars map[string]*query.BindVariable) (*sqltypes.Result, error) {
-	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable") // TODO: shlomi - have no idea if this should work
+	return nil, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "not reachable")
 }
diff --git a/go/vt/vtgate/engine/primitive.go b/go/vt/vtgate/engine/primitive.go
index 8577632fef..966a1cd0f7 100644
--- a/go/vt/vtgate/engine/primitive.go
+++ b/go/vt/vtgate/engine/primitive.go
@@ -126,7 +126,8 @@ type (
 		SetWorkload(querypb.ExecuteOptions_Workload)
 		SetFoundRows(uint64)
 
-		SetDDLStrategy(sqlparser.DDLStrategy)
+		SetDDLStrategy(string)
+		GetDDLStrategy() string
 
 		// SetReadAfterWriteGTID sets the GTID that the user expects a replica to have caught up with before answering a query
 		SetReadAfterWriteGTID(string)
diff --git a/go/vt/vtgate/engine/set.go b/go/vt/vtgate/engine/set.go
index 655f3f1ba4..43401cb8a2 100644
--- a/go/vt/vtgate/engine/set.go
+++ b/go/vt/vtgate/engine/set.go
@@ -404,11 +404,10 @@ func (svss *SysVarSetAware) Execute(vcursor VCursor, env evalengine.ExpressionEn
 		if err != nil {
 			return err
 		}
-		strategy, err := schema.ValidateDDLStrategy(str)
-		if err != nil {
+		if _, _, err := schema.ParseDDLStrategy(str); err != nil {
 			return vterrors.Errorf(vtrpcpb.Code_INVALID_ARGUMENT, "invalid DDL strategy: %s", str)
 		}
-		vcursor.Session().SetDDLStrategy(strategy)
+		vcursor.Session().SetDDLStrategy(str)
 	case sysvars.Charset.Name, sysvars.Names.Name:
 		str, err := svss.evalAsString(env)
 		if err != nil {
diff --git a/go/vt/vtgate/planbuilder/builder.go b/go/vt/vtgate/planbuilder/builder.go
index ef5ff58f30..f383afe211 100644
--- a/go/vt/vtgate/planbuilder/builder.go
+++ b/go/vt/vtgate/planbuilder/builder.go
@@ -322,10 +322,7 @@ func createInstructionFor(query string, stmt sqlparser.Statement, vschema Contex
 		if sqlparser.IsVschemaDDL(stmt) {
 			return buildVSchemaDDLPlan(stmt, vschema)
 		}
-		if sqlparser.IsOnlineSchemaDDL(stmt) {
-			return buildOnlineDDLPlan(query, stmt, vschema)
-		}
-		return buildDDLPlan(query, stmt, vschema)
+		return buildGeneralDDLPlan(query, stmt, vschema)
 	case *sqlparser.Use:
 		return buildUsePlan(stmt, vschema)
 	case *sqlparser.Explain:
diff --git a/go/vt/vtgate/planbuilder/ddl.go b/go/vt/vtgate/planbuilder/ddl.go
index fde83e02ab..50d15339a0 100644
--- a/go/vt/vtgate/planbuilder/ddl.go
+++ b/go/vt/vtgate/planbuilder/ddl.go
@@ -11,27 +11,58 @@ import (
 	"vitess.io/vitess/go/vt/vtgate/engine"
 )
 
-func buildDDLPlan(sql string, stmt sqlparser.DDLStatement, vschema ContextVSchema) (engine.Primitive, error) {
+// buildGeneralDDLPlan builds a general DDL plan, which can be either normal DDL or online DDL.
+// The two behave compeltely differently, and have two very different primitives.
+// We want to be able to dynamically choose between normal/online plans according to Session settings.
+// However, due to caching of plans, we're unable to make that choice right now. In this function we don't have
+// a session context. It's only when we Execute() the primitive that we have that context.
+// This is why we return a compound primitive (DDL) which contains fully populated primitives (Send & OnlineDDL),
+// and which chooses which of the two to invoke at runtime.
+func buildGeneralDDLPlan(sql string, ddlStatement sqlparser.DDLStatement, vschema ContextVSchema) (engine.Primitive, error) {
+	normalDDLPlan, err := buildDDLPlan(sql, ddlStatement, vschema)
+	if err != nil {
+		return nil, err
+	}
+	onlineDDLPlan, err := buildOnlineDDLPlan(sql, ddlStatement, vschema)
+	if err != nil {
+		return nil, err
+	}
+	query := sql
+	// If the query is fully parsed, generate the query from the ast. Otherwise, use the original query
+	if ddlStatement.IsFullyParsed() {
+		query = sqlparser.String(ddlStatement)
+	}
+
+	return &engine.DDL{
+		Keyspace:  normalDDLPlan.Keyspace,
+		SQL:       query,
+		DDL:       ddlStatement,
+		NormalDDL: normalDDLPlan,
+		OnlineDDL: onlineDDLPlan,
+	}, nil
+}
+
+func buildDDLPlan(sql string, ddlStatement sqlparser.DDLStatement, vschema ContextVSchema) (*engine.Send, error) {
 	var table *vindexes.Table
 	var destination key.Destination
 	var keyspace *vindexes.Keyspace
 	var err error
 
-	switch stmt.(type) {
+	switch ddlStatement.(type) {
 	case *sqlparser.CreateIndex:
 		// For Create index, the table must already exist
 		// We should find the target of the query from this tables location
-		table, _, _, _, destination, err = vschema.FindTableOrVindex(stmt.GetTable())
+		table, _, _, _, destination, err = vschema.FindTableOrVindex(ddlStatement.GetTable())
 		keyspace = table.Keyspace
 		if err != nil {
 			return nil, err
 		}
 		if table == nil {
-			return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "table does not exists: %s", stmt.GetTable().Name.String())
+			return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "table does not exists: %s", ddlStatement.GetTable().Name.String())
 		}
-		stmt.SetTable("", table.Name.String())
+		ddlStatement.SetTable("", table.Name.String())
 	case *sqlparser.DDL:
-		destination, keyspace, _, err = vschema.TargetDestination(stmt.GetTable().Qualifier.String())
+		destination, keyspace, _, err = vschema.TargetDestination(ddlStatement.GetTable().Qualifier.String())
 		if err != nil {
 			return nil, err
 		}
@@ -43,8 +74,8 @@ func buildDDLPlan(sql string, stmt sqlparser.DDLStatement, vschema ContextVSchem
 
 	query := sql
 	// If the query is fully parsed, generate the query from the ast. Otherwise, use the original query
-	if stmt.IsFullyParsed() {
-		query = sqlparser.String(stmt)
+	if ddlStatement.IsFullyParsed() {
+		query = sqlparser.String(ddlStatement)
 	}
 
 	return &engine.Send{
@@ -56,28 +87,29 @@ func buildDDLPlan(sql string, stmt sqlparser.DDLStatement, vschema ContextVSchem
 	}, nil
 }
 
-func buildOnlineDDLPlan(query string, stmt sqlparser.DDLStatement, vschema ContextVSchema) (engine.Primitive, error) {
-
-	_, keyspace, _, err := vschema.TargetDestination(stmt.GetTable().Qualifier.String())
+func buildOnlineDDLPlan(query string, ddlStatement sqlparser.DDLStatement, vschema ContextVSchema) (*engine.OnlineDDL, error) {
+	_, keyspace, _, err := vschema.TargetDestination(ddlStatement.GetTable().Qualifier.String())
 	if err != nil {
 		return nil, err
 	}
-	if stmt.GetOnlineHint() == nil {
-		return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "not an online DDL: %s", query)
+
+	strategy := schema.DDLStrategyNormal
+	options := ""
+	if ddlStatement.GetOnlineHint() != nil {
+		strategy = ddlStatement.GetOnlineHint().Strategy
+		options = ddlStatement.GetOnlineHint().Options
 	}
-	switch stmt.GetOnlineHint().Strategy {
-	case schema.DDLStrategyGhost, schema.DDLStrategyPTOSC: // OK, do nothing
-	case schema.DDLStrategyNormal:
-		return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "not an online DDL strategy")
+	switch strategy {
+	case schema.DDLStrategyGhost, schema.DDLStrategyPTOSC, schema.DDLStrategyNormal: // OK, do nothing
 	default:
-		return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "unknown online DDL strategy: '%v'", stmt.GetOnlineHint().Strategy)
+		return nil, vterrors.Errorf(vtrpc.Code_INVALID_ARGUMENT, "unknown online DDL strategy: '%v'", strategy)
 	}
 	return &engine.OnlineDDL{
 		Keyspace: keyspace,
-		DDL:      stmt,
+		DDL:      ddlStatement,
 		SQL:      query,
-		Strategy: stmt.GetOnlineHint().Strategy,
-		Options:  stmt.GetOnlineHint().Options,
+		Strategy: strategy,
+		Options:  options,
 	}, nil
 }
 
diff --git a/go/vt/vtgate/plugin_mysql_server.go b/go/vt/vtgate/plugin_mysql_server.go
index 317f100e55..905f4b3672 100644
--- a/go/vt/vtgate/plugin_mysql_server.go
+++ b/go/vt/vtgate/plugin_mysql_server.go
@@ -339,7 +339,8 @@ func (vh *vtgateHandler) session(c *mysql.Conn) *vtgatepb.Session {
 				IncludedFields: querypb.ExecuteOptions_ALL,
 				Workload:       querypb.ExecuteOptions_Workload(mysqlDefaultWorkload),
 			},
-			Autocommit: true,
+			Autocommit:  true,
+			DDLStrategy: *defaultDDLStrategy,
 		}
 		if c.Capabilities&mysql.CapabilityClientFoundRows != 0 {
 			session.Options.ClientFoundRows = true
diff --git a/go/vt/vtgate/safe_session.go b/go/vt/vtgate/safe_session.go
index a5ef0ebb03..7b6c610446 100644
--- a/go/vt/vtgate/safe_session.go
+++ b/go/vt/vtgate/safe_session.go
@@ -22,7 +22,6 @@ import (
 	"time"
 
 	"github.com/golang/protobuf/proto"
-	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/vterrors"
 
 	querypb "vitess.io/vitess/go/vt/proto/query"
@@ -455,10 +454,17 @@ func (session *SafeSession) ResetShard(tabletAlias *topodatapb.TabletAlias) erro
 }
 
 // SetDDLStrategy set the DDLStrategy setting.
-func (session *SafeSession) SetDDLStrategy(strategy sqlparser.DDLStrategy) {
+func (session *SafeSession) SetDDLStrategy(strategy string) {
 	session.mu.Lock()
 	defer session.mu.Unlock()
-	session.DDLStrategy = string(strategy)
+	session.DDLStrategy = strategy
+}
+
+// GetDDLStrategy returns the DDLStrategy value.
+func (session *SafeSession) GetDDLStrategy() string {
+	session.mu.Lock()
+	defer session.mu.Unlock()
+	return session.DDLStrategy
 }
 
 // SetReadAfterWriteGTID set the ReadAfterWriteGtid setting.
diff --git a/go/vt/vtgate/vcursor_impl.go b/go/vt/vtgate/vcursor_impl.go
index ddfd518f9d..41e86ef624 100644
--- a/go/vt/vtgate/vcursor_impl.go
+++ b/go/vt/vtgate/vcursor_impl.go
@@ -591,10 +591,15 @@ func (vc *vcursorImpl) SetFoundRows(foundRows uint64) {
 }
 
 // SetReadAfterWriteGTID implements the SessionActions interface
-func (vc *vcursorImpl) SetDDLStrategy(strategy sqlparser.DDLStrategy) {
+func (vc *vcursorImpl) SetDDLStrategy(strategy string) {
 	vc.safeSession.SetDDLStrategy(strategy)
 }
 
+// SetReadAfterWriteGTID implements the SessionActions interface
+func (vc *vcursorImpl) GetDDLStrategy() string {
+	return vc.safeSession.GetDDLStrategy()
+}
+
 // SetReadAfterWriteGTID implements the SessionActions interface
 func (vc *vcursorImpl) SetReadAfterWriteGTID(vtgtid string) {
 	vc.safeSession.SetReadAfterWriteGTID(vtgtid)
diff --git a/go/vt/vtgate/vtgate.go b/go/vt/vtgate/vtgate.go
index f3287900d3..2df81025c3 100644
--- a/go/vt/vtgate/vtgate.go
+++ b/go/vt/vtgate/vtgate.go
@@ -36,6 +36,7 @@ import (
 	"vitess.io/vitess/go/vt/key"
 	"vitess.io/vitess/go/vt/log"
 	"vitess.io/vitess/go/vt/logutil"
+	"vitess.io/vitess/go/vt/schema"
 	"vitess.io/vitess/go/vt/servenv"
 	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/srvtopo"
@@ -60,6 +61,7 @@ var (
 	_                  = flag.Bool("disable_local_gateway", false, "deprecated: if specified, this process will not route any queries to local tablets in the local cell")
 	maxMemoryRows      = flag.Int("max_memory_rows", 300000, "Maximum number of rows that will be held in memory for intermediate results as well as the final result.")
 	warnMemoryRows     = flag.Int("warn_memory_rows", 30000, "Warning threshold for in-memory results. A row count higher than this amount will cause the VtGateWarnings.ResultsExceeded counter to be incremented.")
+	defaultDDLStrategy = flag.String("ddl-strategy", "", "Set default strategy for DDL statements. Override with @@ddl_strategy session variable")
 
 	// TODO(deepthi): change these two vars to unexported and move to healthcheck.go when LegacyHealthcheck is removed
 
@@ -165,6 +167,9 @@ func Init(ctx context.Context, serv srvtopo.Server, cell string, tabletTypesToWa
 		}
 	}
 
+	if _, _, err := schema.ParseDDLStrategy(*defaultDDLStrategy); err != nil {
+		log.Fatalf("Invalid value for --ddl-strategy: %v", err.Error())
+	}
 	tc := NewTxConn(gw, getTxMode())
 	// ScatterConn depends on TxConn to perform forced rollbacks.
 	sc := NewScatterConn("VttabletCall", tc, gw)


Test patch:
diff --git a/go/test/endtoend/cluster/vtctlclient_process.go b/go/test/endtoend/cluster/vtctlclient_process.go
index 6de9306c88..c6d52d004f 100644
--- a/go/test/endtoend/cluster/vtctlclient_process.go
+++ b/go/test/endtoend/cluster/vtctlclient_process.go
@@ -48,16 +48,17 @@ func (vtctlclient *VtctlClientProcess) InitShardMaster(Keyspace string, Shard st
 }
 
 // ApplySchemaWithOutput applies SQL schema to the keyspace
-func (vtctlclient *VtctlClientProcess) ApplySchemaWithOutput(Keyspace string, SQL string) (result string, err error) {
+func (vtctlclient *VtctlClientProcess) ApplySchemaWithOutput(Keyspace string, SQL string, ddlStrategy string) (result string, err error) {
 	return vtctlclient.ExecuteCommandWithOutput(
 		"ApplySchema",
 		"-sql", SQL,
+		"-ddl_strategy", ddlStrategy,
 		Keyspace)
 }
 
 // ApplySchema applies SQL schema to the keyspace
 func (vtctlclient *VtctlClientProcess) ApplySchema(Keyspace string, SQL string) (err error) {
-	_, err = vtctlclient.ApplySchemaWithOutput(Keyspace, SQL)
+	_, err = vtctlclient.ApplySchemaWithOutput(Keyspace, SQL, "")
 	return err
 }
 
diff --git a/go/test/endtoend/onlineddl/onlineddl_test.go b/go/test/endtoend/onlineddl/onlineddl_test.go
index 3ec883c707..8395333b27 100644
--- a/go/test/endtoend/onlineddl/onlineddl_test.go
+++ b/go/test/endtoend/onlineddl/onlineddl_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package onlineddl
 
 import (
+	"context"
 	"flag"
 	"fmt"
 	"os"
@@ -26,20 +27,25 @@ import (
 	"testing"
 	"time"
 
+	"vitess.io/vitess/go/mysql"
+	"vitess.io/vitess/go/sqltypes"
 	"vitess.io/vitess/go/vt/schema"
 
+	"vitess.io/vitess/go/test/endtoend/cluster"
+
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"vitess.io/vitess/go/test/endtoend/cluster"
 )
 
 var (
 	clusterInstance       *cluster.LocalProcessCluster
+	vtParams              mysql.ConnParams
 	hostname              = "localhost"
 	keyspaceName          = "ks"
 	cell                  = "zone1"
 	schemaChangeDirectory = ""
 	totalTableCount       = 4
+	ddlStrategyUnchanged  = "-"
 	createTable           = `
 		CREATE TABLE %s (
 		id BIGINT(20) not NULL,
@@ -50,15 +56,19 @@ var (
 	alterTableNormalStatement = `
 		ALTER TABLE %s
 		ADD COLUMN non_online INT UNSIGNED NOT NULL`
+	// A trivial statement which must succeed and does not change the schema
+	alterTableTrivialStatement = `
+		ALTER TABLE %s
+		ENGINE=InnoDB`
 	// The following statement is valid
 	alterTableSuccessfulStatement = `
-		ALTER WITH 'gh-ost' TABLE %s
+		ALTER TABLE %s
 		MODIFY id BIGINT UNSIGNED NOT NULL,
 		ADD COLUMN ghost_col INT NOT NULL,
 		ADD INDEX idx_msg(msg)`
 	// The following statement will fail because gh-ost requires some shared unique key
 	alterTableFailedStatement = `
-		ALTER WITH 'gh-ost' TABLE %s
+		ALTER TABLE %s
 		DROP PRIMARY KEY,
 		DROP COLUMN ghost_col`
 	alterTableThrottlingStatement = `
@@ -95,6 +105,9 @@ func TestMain(m *testing.M) {
 		clusterInstance.VtTabletExtraArgs = []string{
 			"-migration_check_interval", "5s",
 		}
+		clusterInstance.VtGateExtraArgs = []string{
+			"-ddl-strategy", "gh-ost",
+		}
 
 		if err := clusterInstance.StartTopo(); err != nil {
 			return 1, err
@@ -111,6 +124,21 @@ func TestMain(m *testing.M) {
 		if err := clusterInstance.StartKeyspace(*keyspace, []string{"1"}, 1, false); err != nil {
 			return 1, err
 		}
+
+		vtgateInstance := clusterInstance.NewVtgateInstance()
+		// set the gateway we want to use
+		vtgateInstance.GatewayImplementation = "tabletgateway"
+		// Start vtgate
+		if err := vtgateInstance.Setup(); err != nil {
+			return 1, err
+		}
+		// ensure it is torn down during cluster TearDown
+		clusterInstance.VtgateProcess = *vtgateInstance
+		vtParams = mysql.ConnParams{
+			Host: clusterInstance.Hostname,
+			Port: clusterInstance.VtgateMySQLPort,
+		}
+
 		return m.Run(), nil
 	}()
 	if err != nil {
@@ -127,23 +155,29 @@ func TestSchemaChange(t *testing.T) {
 	assert.Equal(t, 2, len(clusterInstance.Keyspaces[0].Shards))
 	testWithInitialSchema(t)
 	{
-		_ = testAlterTable(t, alterTableNormalStatement, false, "non_online")
+		_ = testAlterTable(t, alterTableNormalStatement, string(schema.DDLStrategyNormal), "vtctl", "non_online")
+	}
+	{
+		uuid := testAlterTable(t, alterTableSuccessfulStatement, ddlStrategyUnchanged, "vtgate", "ghost_col")
+		checkRecentMigrations(t, uuid, schema.OnlineDDLStatusComplete)
+		checkCancelMigration(t, uuid, false)
+		checkRetryMigration(t, uuid, false)
 	}
 	{
-		uuid := testAlterTable(t, alterTableSuccessfulStatement, true, "ghost_col")
+		uuid := testAlterTable(t, alterTableTrivialStatement, string(schema.DDLStrategyGhost), "vtctl", "ghost_col")
 		checkRecentMigrations(t, uuid, schema.OnlineDDLStatusComplete)
 		checkCancelMigration(t, uuid, false)
 		checkRetryMigration(t, uuid, false)
 	}
 	{
-		uuid := testAlterTable(t, alterTableThrottlingStatement, true, "ghost_col")
+		uuid := testAlterTable(t, alterTableThrottlingStatement, string(schema.DDLStrategyGhost), "vtgate", "ghost_col")
 		checkRecentMigrations(t, uuid, schema.OnlineDDLStatusRunning)
 		checkCancelMigration(t, uuid, true)
 		time.Sleep(2 * time.Second)
 		checkRecentMigrations(t, uuid, schema.OnlineDDLStatusFailed)
 	}
 	{
-		uuid := testAlterTable(t, alterTableFailedStatement, true, "ghost_col")
+		uuid := testAlterTable(t, alterTableFailedStatement, string(schema.DDLStrategyGhost), "vtgate", "ghost_col")
 		checkRecentMigrations(t, uuid, schema.OnlineDDLStatusFailed)
 		checkCancelMigration(t, uuid, false)
 		checkRetryMigration(t, uuid, true)
@@ -164,19 +198,31 @@ func testWithInitialSchema(t *testing.T) {
 }
 
 // testAlterTable runs an online DDL, ALTER statement
-func testAlterTable(t *testing.T, alterStatement string, isOnlineDDL bool, expectColumn string) (uuid string) {
+func testAlterTable(t *testing.T, alterStatement string, ddlStrategy string, executeStrategy string, expectColumn string) (uuid string) {
 	tableName := fmt.Sprintf("vt_onlineddl_test_%02d", 3)
 	sqlQuery := fmt.Sprintf(alterStatement, tableName)
-	uuid, err := clusterInstance.VtctlclientProcess.ApplySchemaWithOutput(keyspaceName, sqlQuery)
-	require.Nil(t, err)
+	if executeStrategy == "vtgate" {
+		row := vtgateExec(t, ddlStrategy, sqlQuery, "").Named().Row()
+		if row != nil {
+			uuid = row.AsString("uuid", "")
+		}
+	} else {
+		var err error
+		ddlStrategyArg := ""
+		if ddlStrategy != ddlStrategyUnchanged {
+			ddlStrategyArg = ddlStrategy
+		}
+		uuid, err = clusterInstance.VtctlclientProcess.ApplySchemaWithOutput(keyspaceName, sqlQuery, ddlStrategyArg)
+		assert.NoError(t, err)
+	}
 	uuid = strings.TrimSpace(uuid)
-	if isOnlineDDL {
-		require.NotEmpty(t, uuid)
-		// Migration is asynchronous. Give it some time.
+	fmt.Println("# Generated UUID (for debug purposes):")
+	fmt.Printf("<%s>\n", uuid)
+
+	if ddlStrategy != string(schema.DDLStrategyNormal) {
 		time.Sleep(time.Second * 20)
-	} else {
-		require.Empty(t, uuid)
 	}
+
 	checkMigratedTable(t, tableName, expectColumn)
 	return uuid
 }
@@ -267,3 +313,26 @@ func getCreateTableStatement(t *testing.T, tablet *cluster.Vttablet, tableName s
 	statement = queryResult.Rows[0][1].ToString()
 	return statement
 }
+
+func vtgateExec(t *testing.T, ddlStrategy string, query string, expectError string) *sqltypes.Result {
+	t.Helper()
+
+	ctx := context.Background()
+	conn, err := mysql.Connect(ctx, &vtParams)
+	require.Nil(t, err)
+	defer conn.Close()
+
+	if ddlStrategy != ddlStrategyUnchanged {
+		setSession := fmt.Sprintf("set @@ddl_strategy='%s'", ddlStrategy)
+		_, err := conn.ExecuteFetch(setSession, 1000, true)
+		assert.NoError(t, err)
+	}
+	qr, err := conn.ExecuteFetch(query, 1000, true)
+	if expectError == "" {
+		require.NoError(t, err)
+	} else {
+		require.Error(t, err, "error should not be nil")
+		assert.Contains(t, err.Error(), expectError, "Unexpected error")
+	}
+	return qr
+}
diff --git a/go/vt/schema/online_ddl_test.go b/go/vt/schema/online_ddl_test.go
index f0bf0e9b5f..d35bc9d425 100644
--- a/go/vt/schema/online_ddl_test.go
+++ b/go/vt/schema/online_ddl_test.go
@@ -19,6 +19,8 @@ package schema
 import (
 	"testing"
 
+	"vitess.io/vitess/go/vt/sqlparser"
+
 	"github.com/stretchr/testify/assert"
 )
 
@@ -27,24 +29,38 @@ func TestCreateUUID(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestValidateDDLStrategy(t *testing.T) {
-	{
-		strategy, err := ValidateDDLStrategy("gh-ost")
-		assert.NoError(t, err)
-		assert.Equal(t, DDLStrategyGhost, strategy)
+func TestParseDDLStrategy(t *testing.T) {
+	tt := []struct {
+		strategyVariable string
+		strategy         sqlparser.DDLStrategy
+		options          string
+		err              error
+	}{
+		{
+			strategyVariable: "gh-ost",
+			strategy:         DDLStrategyGhost,
+		},
+		{
+			strategyVariable: "pt-osc",
+			strategy:         DDLStrategyPTOSC,
+		},
+		{
+			strategy: DDLStrategyNormal,
+		},
+		{
+			strategyVariable: "gh-ost --max-load=Threads_running=100 --allow-master",
+			strategy:         DDLStrategyGhost,
+			options:          "--max-load=Threads_running=100 --allow-master",
+		},
 	}
-	{
-		strategy, err := ValidateDDLStrategy("pt-osc")
-		assert.NoError(t, err)
-		assert.Equal(t, DDLStrategyPTOSC, strategy)
-	}
-	{
-		strategy, err := ValidateDDLStrategy("")
+	for _, ts := range tt {
+		strategy, options, err := ParseDDLStrategy(ts.strategyVariable)
 		assert.NoError(t, err)
-		assert.Equal(t, DDLStrategyNormal, strategy)
+		assert.Equal(t, ts.strategy, strategy)
+		assert.Equal(t, ts.options, options)
 	}
 	{
-		_, err := ValidateDDLStrategy("other")
+		_, _, err := ParseDDLStrategy("other")
 		assert.Error(t, err)
 	}
 }
diff --git a/go/vt/schemamanager/tablet_executor_test.go b/go/vt/schemamanager/tablet_executor_test.go
index 6928dbf30c..b2c873f78b 100644
--- a/go/vt/schemamanager/tablet_executor_test.go
+++ b/go/vt/schemamanager/tablet_executor_test.go
@@ -27,8 +27,12 @@ import (
 	"vitess.io/vitess/go/vt/mysqlctl/tmutils"
 	tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata"
 	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
+	"vitess.io/vitess/go/vt/schema"
+	"vitess.io/vitess/go/vt/sqlparser"
 	"vitess.io/vitess/go/vt/topo/memorytopo"
 	"vitess.io/vitess/go/vt/wrangler"
+
+	"github.com/stretchr/testify/assert"
 )
 
 var (
@@ -216,3 +220,60 @@ func TestTabletExecutorExecute(t *testing.T) {
 		t.Fatalf("execute should fail, call execute.Open first")
 	}
 }
+
+func TestIsOnlineSchemaDDL(t *testing.T) {
+	tt := []struct {
+		query       string
+		ddlStrategy string
+		isOnlineDDL bool
+		strategy    sqlparser.DDLStrategy
+		options     string
+	}{
+		{
+			query:       "CREATE TABLE t(id int)",
+			isOnlineDDL: false,
+		},
+		{
+			query:       "CREATE TABLE t(id int)",
+			ddlStrategy: "gh-ost",
+			isOnlineDDL: false,
+		},
+		{
+			query:       "ALTER TABLE t ADD COLUMN i INT",
+			ddlStrategy: "",
+			isOnlineDDL: false,
+		},
+		{
+			query:       "ALTER TABLE t ADD COLUMN i INT",
+			ddlStrategy: "gh-ost",
+			isOnlineDDL: true,
+			strategy:    schema.DDLStrategyGhost,
+		},
+		{
+			query:       "ALTER TABLE t ADD COLUMN i INT",
+			ddlStrategy: "gh-ost --max-load=Threads_running=100",
+			isOnlineDDL: true,
+			strategy:    schema.DDLStrategyGhost,
+			options:     "--max-load=Threads_running=100",
+		},
+	}
+
+	for _, ts := range tt {
+		e := &TabletExecutor{}
+		err := e.SetDDLStrategy(ts.ddlStrategy)
+		assert.NoError(t, err)
+
+		stmt, err := sqlparser.Parse(ts.query)
+		assert.NoError(t, err)
+
+		ddl, ok := stmt.(*sqlparser.DDL)
+		assert.True(t, ok)
+
+		isOnlineDDL, strategy, options := e.isOnlineSchemaDDL(ddl)
+		assert.Equal(t, ts.isOnlineDDL, isOnlineDDL)
+		if isOnlineDDL {
+			assert.Equal(t, ts.strategy, strategy)
+			assert.Equal(t, ts.options, options)
+		}
+	}
+}
diff --git a/go/vt/vtgate/engine/fake_vcursor_test.go b/go/vt/vtgate/engine/fake_vcursor_test.go
index cea216e331..43cd0b775c 100644
--- a/go/vt/vtgate/engine/fake_vcursor_test.go
+++ b/go/vt/vtgate/engine/fake_vcursor_test.go
@@ -57,7 +57,11 @@ type noopVCursor struct {
 	ctx context.Context
 }
 
-func (t noopVCursor) SetDDLStrategy(strategy sqlparser.DDLStrategy) {
+func (t noopVCursor) SetDDLStrategy(strategy string) {
+	panic("implement me")
+}
+
+func (t noopVCursor) GetDDLStrategy() string {
 	panic("implement me")
 }
 
diff --git a/go/vt/vtgate/planbuilder/testdata/ddl_cases.txt b/go/vt/vtgate/planbuilder/testdata/ddl_cases.txt
index 6078915fcf..5ab474c585 100644
--- a/go/vt/vtgate/planbuilder/testdata/ddl_cases.txt
+++ b/go/vt/vtgate/planbuilder/testdata/ddl_cases.txt
@@ -4,15 +4,12 @@
   "QueryType": "DDL",
   "Original": "create table t1(id bigint, primary key(id))",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "main",
       "Sharded": false
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "create table t1(id bigint, primary key(id))",
-    "SingleShardOnly": false
+    "Query": "create table t1(id bigint, primary key(id))"
   }
 }
 
@@ -22,15 +19,12 @@
   "QueryType": "DDL",
   "Original": "create table user.t1(id bigint, primary key(id))",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "create table user.t1(id bigint, primary key(id))",
-    "SingleShardOnly": false
+    "Query": "create table user.t1(id bigint, primary key(id))"
   }
 }
 
@@ -40,15 +34,12 @@
   "QueryType": "DDL",
   "Original": "create table a(id int)",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "main",
       "Sharded": false
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "create table a(id int)",
-    "SingleShardOnly": false
+    "Query": "create table a(id int)"
   }
 }
 
@@ -62,15 +53,12 @@
   "QueryType": "DDL",
   "Original": "alter table a ADD id int",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "main",
       "Sharded": false
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "alter table a ADD id int",
-    "SingleShardOnly": false
+    "Query": "alter table a ADD id int"
   }
 }
 
@@ -80,15 +68,12 @@
   "QueryType": "DDL",
   "Original": "alter table user.b ADD id int",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "alter table user.b ADD id int",
-    "SingleShardOnly": false
+    "Query": "alter table user.b ADD id int"
   }
 }
 
@@ -140,15 +125,12 @@
   "QueryType": "DDL",
   "Original": "create index a on user(id)",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "create index a on user (id)",
-    "SingleShardOnly": false
+    "Query": "create index a on user (id)"
   }
 }
 
@@ -158,15 +140,12 @@
   "QueryType": "DDL",
   "Original": "create index a on user.user(id)",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "user",
       "Sharded": true
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "create index a on user (id)",
-    "SingleShardOnly": false
+    "Query": "create index a on user (id)"
   }
 }
 
@@ -176,14 +155,11 @@
   "QueryType": "DDL",
   "Original": "create index a on main.unknown(id)",
   "Instructions": {
-    "OperatorType": "Send",
+    "OperatorType": "DDL",
     "Keyspace": {
       "Name": "main",
       "Sharded": false
     },
-    "TargetDestination": "AllShards()",
-    "IsDML": false,
-    "Query": "create index a on unknown (id)",
-    "SingleShardOnly": false
+    "Query": "create index a on unknown (id)"
   }
 }