Skip to content

Commit 4425351

Browse files
maxenglandercodexcursoragentCopilot
authored
ApplySchema: add session variable options (#20654)
Signed-off-by: Max Englander <max@planetscale.com> Signed-off-by: Max Englander <max.englander@gmail.com> Co-authored-by: GPT-5.6 Sol <noreply@openai.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 4faa1e0 commit 4425351

18 files changed

Lines changed: 2473 additions & 620 deletions

File tree

changelog/25.0/25.0.0/summary.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- [New `--demote-primary-lock-wait-timeout` flag](#vttablet-demote-primary-lock-wait-timeout)
3939
- [Schema engine table-count limit is now configurable](#vttablet-schema-max-table-count)
4040
- [Skip MySQL version check when restoring from a mysql-shell backup](#vttablet-mysql-shell-restore-skip-version-check)
41+
- [ApplySchema session variables](#vttablet-applyschema-session-variables)
4142
- **[Backup/Restore](#minor-changes-backup)**
4243
- [Chunked backup/restore for the builtinbackupengine](#backup-chunked-builtin)
4344
- [Slow clean mysqld shutdowns no longer fail backups](#backup-mysqld-shutdown-timeout)
@@ -341,6 +342,41 @@ Because mysql-shell performs a logical restore, its backups are not tied to the
341342

342343
**Impact**: With this flag set, VTTablet may select and restore a `mysqlshell` backup whose MySQL version would otherwise be rejected as incompatible. Leave it unset to preserve the existing behavior.
343344

345+
#### <a id="vttablet-applyschema-session-variables"/>ApplySchema session variables</a>
346+
347+
`ApplySchema` now accepts repeatable `--session-variable name=value` DDL
348+
strategy options. The assignments use MySQL `SESSION` scope and are applied in
349+
the order supplied.
350+
351+
For the `direct` strategy, the variables apply to the dedicated DBA connection
352+
that executes the requested schema statements. For Online DDL, they apply to
353+
the dedicated connections used for:
354+
355+
- scheduler-executed direct DDL;
356+
- VReplication shadow-table creation, alteration, and `AUTO_INCREMENT`
357+
adjustment;
358+
- declarative comparison-table DDL; and
359+
- online view artifact creation and its view swap.
360+
361+
The variables do not apply to the pooled connections used during a
362+
VReplication cutover. In particular, they do not affect sentry-table DDL or the
363+
final `RENAME TABLE` that swaps the original and shadow tables.
364+
365+
Each affected connection's previous values are restored afterward. Invalid,
366+
duplicate, or denied variable names and failed assignments stop the operation
367+
before schema DDL executes on that connection. `sql_log_bin`,
368+
`foreign_key_checks`, and `gtid_next` are denied.
369+
370+
**Compatibility note:** `--session-variable` requires vtctld and vttablet at
371+
v25 or newer. On a mixed-version cluster, an upgraded caller can send the new
372+
`session_variables` RPC field (or Online DDL options) to an older tablet that
373+
does not understand them. The tablet may still run the DDL while skipping the
374+
requested session state, so the option can appear to succeed without effect.
375+
Upgrade vtctld, vtctldclient, vtgate and vttablet before executing a schema
376+
change with `--session-variable`.
377+
378+
See [#20654](https://github.com/vitessio/vitess/pull/20654) for details.
379+
344380
### <a id="minor-changes-backup"/>Backup/Restore</a>
345381

346382
#### <a id="backup-chunked-builtin"/>Chunked backup/restore for the `builtinbackupengine`</a>

go/cmd/vtctldclient/command/schema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ func commandValidateSchemaShard(cmd *cobra.Command, args []string) error {
439439
}
440440

441441
func init() {
442-
utils.SetFlagStringVar(ApplySchema.Flags(), &applySchemaOptions.DDLStrategy, "ddl-strategy", string(schema.DDLStrategyDirect), "Online DDL strategy, compatible with @@ddl_strategy session variable (examples: 'direct', 'mysql', 'vitess --postpone-completion'.")
442+
utils.SetFlagStringVar(ApplySchema.Flags(), &applySchemaOptions.DDLStrategy, "ddl-strategy", string(schema.DDLStrategyDirect), "Online DDL strategy and options, compatible with the @@ddl_strategy session variable. Examples: 'direct', 'mysql', 'vitess --postpone-completion', 'vitess --session-variable sql_mode=ANSI'. The repeatable --session-variable name=value option applies variables in SESSION scope before DDL.")
443443
ApplySchema.Flags().StringSliceVar(&applySchemaOptions.UUIDList, "uuid", nil, "Optional, comma-delimited, repeatable, explicit UUIDs for migration. If given, must match number of DDL changes.")
444444
ApplySchema.Flags().StringVar(&applySchemaOptions.MigrationContext, "migration-context", "", "For Online DDL, optionally supply a custom unique string used as context for the migration(s) in this command. By default a unique context is auto-generated by Vitess.")
445445
ApplySchema.Flags().DurationVar(&applySchemaOptions.WaitReplicasTimeout, "wait-replicas-timeout", grpcvtctldserver.DefaultWaitReplicasTimeout, "Amount of time to wait for replicas to receive the schema change via replication.")
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
Copyright 2026 The Vitess Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package sessionvariable
18+
19+
import (
20+
"flag"
21+
"fmt"
22+
"os"
23+
"path"
24+
"strings"
25+
"testing"
26+
"time"
27+
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
30+
31+
"vitess.io/vitess/go/mysql"
32+
"vitess.io/vitess/go/test/endtoend/cluster"
33+
"vitess.io/vitess/go/test/endtoend/onlineddl"
34+
"vitess.io/vitess/go/vt/schema"
35+
)
36+
37+
var (
38+
clusterInstance *cluster.LocalProcessCluster
39+
shards []cluster.Shard
40+
vtParams mysql.ConnParams
41+
42+
hostname = "localhost"
43+
keyspaceName = "ks"
44+
cell = "zone1"
45+
schemaChangeDirectory = ""
46+
migrationWaitTimeout = 60 * time.Second
47+
48+
// Zero-date defaults are rejected under the default MySQL sql_mode. Setting
49+
// sql_mode via --session-variable is what makes this CREATE succeed.
50+
createZeroDateTable = `
51+
CREATE TABLE %s (
52+
id INT NOT NULL,
53+
d DATE DEFAULT '0000-00-00',
54+
PRIMARY KEY (id)
55+
) ENGINE=InnoDB`
56+
createBaseTable = `
57+
CREATE TABLE %s (
58+
id INT NOT NULL,
59+
PRIMARY KEY (id)
60+
) ENGINE=InnoDB`
61+
// Adding a zero-date default must fail during VReplication shadow-table ALTER
62+
// unless sql_mode allows invalid dates.
63+
alterAddZeroDateColumn = `ALTER TABLE %s ADD COLUMN d DATE DEFAULT '0000-00-00'`
64+
dropTable = `DROP TABLE IF EXISTS %s`
65+
66+
sessionVariableStrategy = "direct --session-variable sql_mode=ALLOW_INVALID_DATES"
67+
onlineSessionStrategy = "vitess --session-variable sql_mode=ALLOW_INVALID_DATES"
68+
)
69+
70+
func TestMain(m *testing.M) {
71+
flag.Parse()
72+
73+
exitcode, err := func() (int, error) {
74+
clusterInstance = cluster.NewCluster(cell, hostname)
75+
schemaChangeDirectory = path.Join("/tmp", fmt.Sprintf("schema_change_dir_%d", clusterInstance.GetAndReserveTabletUID()))
76+
defer os.RemoveAll(schemaChangeDirectory)
77+
defer clusterInstance.Teardown()
78+
79+
if _, err := os.Stat(schemaChangeDirectory); os.IsNotExist(err) {
80+
_ = os.Mkdir(schemaChangeDirectory, 0o700)
81+
}
82+
83+
clusterInstance.VtctldExtraArgs = []string{
84+
"--schema-change-dir", schemaChangeDirectory,
85+
"--schema-change-controller", "local",
86+
"--schema-change-check-interval", "1s",
87+
}
88+
clusterInstance.VtTabletExtraArgs = []string{
89+
"--heartbeat-interval", "250ms",
90+
"--migration-check-interval", "2s",
91+
}
92+
93+
if err := clusterInstance.StartTopo(); err != nil {
94+
return 1, err
95+
}
96+
97+
keyspace := &cluster.Keyspace{Name: keyspaceName}
98+
if err := clusterInstance.StartUnshardedKeyspace(*keyspace, 1, false, cell); err != nil {
99+
return 1, err
100+
}
101+
102+
vtgateInstance := clusterInstance.NewVtgateInstance()
103+
if err := vtgateInstance.Setup(); err != nil {
104+
return 1, err
105+
}
106+
clusterInstance.VtgateProcess = *vtgateInstance
107+
vtParams = mysql.ConnParams{
108+
Host: clusterInstance.Hostname,
109+
Port: clusterInstance.VtgateMySQLPort,
110+
}
111+
112+
return m.Run(), nil
113+
}()
114+
if err != nil {
115+
fmt.Printf("%v\n", err)
116+
os.Exit(1)
117+
}
118+
os.Exit(exitcode)
119+
}
120+
121+
// TestVtctldclientDirectSessionVariable verifies ApplySchema --ddl-strategy with
122+
// --session-variable applies SESSION sql_mode before direct DDL.
123+
func TestVtctldclientDirectSessionVariable(t *testing.T) {
124+
tableName := "vtctldclient_session_var"
125+
createSQL := fmt.Sprintf(createZeroDateTable, tableName)
126+
dropSQL := fmt.Sprintf(dropTable, tableName)
127+
t.Cleanup(func() {
128+
_, _ = clusterInstance.VtctldClientProcess.ApplySchemaWithOutput(
129+
keyspaceName,
130+
dropSQL,
131+
cluster.ApplySchemaParams{DDLStrategy: "direct"},
132+
)
133+
})
134+
135+
t.Run("without session variable", func(t *testing.T) {
136+
output, err := clusterInstance.VtctldClientProcess.ApplySchemaWithOutput(
137+
keyspaceName,
138+
createSQL,
139+
cluster.ApplySchemaParams{DDLStrategy: "direct"},
140+
)
141+
require.Error(t, err)
142+
assert.True(t,
143+
strings.Contains(output, "Invalid default value") || strings.Contains(err.Error(), "Invalid default value"),
144+
"expected zero-date rejection, got output=%q err=%v", output, err,
145+
)
146+
})
147+
148+
t.Run("with session variable", func(t *testing.T) {
149+
_, err := clusterInstance.VtctldClientProcess.ApplySchemaWithOutput(
150+
keyspaceName,
151+
createSQL,
152+
cluster.ApplySchemaParams{DDLStrategy: sessionVariableStrategy},
153+
)
154+
require.NoError(t, err)
155+
assertTableExists(t, tableName)
156+
})
157+
}
158+
159+
// TestVtgateOnlineSessionVariable verifies @@ddl_strategy --session-variable is
160+
// applied on the VReplication shadow-table path (initVreplicationOriginalMigration),
161+
// not only on CREATE TABLE which executes directly.
162+
func TestVtgateOnlineSessionVariable(t *testing.T) {
163+
require.NoError(t, clusterInstance.WaitForTabletsToHealthyInVtgate())
164+
shards = clusterInstance.Keyspaces[0].Shards
165+
166+
tableName := "vtgate_session_var"
167+
createSQL := fmt.Sprintf(createBaseTable, tableName)
168+
alterSQL := fmt.Sprintf(alterAddZeroDateColumn, tableName)
169+
dropSQL := fmt.Sprintf(dropTable, tableName)
170+
t.Cleanup(func() {
171+
_, _ = clusterInstance.VtctldClientProcess.ApplySchemaWithOutput(
172+
keyspaceName,
173+
dropSQL,
174+
cluster.ApplySchemaParams{DDLStrategy: "direct"},
175+
)
176+
})
177+
178+
_, err := clusterInstance.VtctldClientProcess.ApplySchemaWithOutput(
179+
keyspaceName,
180+
createSQL,
181+
cluster.ApplySchemaParams{DDLStrategy: "direct"},
182+
)
183+
require.NoError(t, err)
184+
assertTableExists(t, tableName)
185+
186+
t.Run("without session variable", func(t *testing.T) {
187+
uuid := submitOnlineDDL(t, "vitess", alterSQL)
188+
status := onlineddl.WaitForMigrationStatus(
189+
t, &vtParams, shards, uuid, migrationWaitTimeout,
190+
schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed,
191+
)
192+
require.Equal(t, schema.OnlineDDLStatusFailed, status)
193+
assertColumnMissing(t, tableName, "d")
194+
})
195+
196+
t.Run("with session variable", func(t *testing.T) {
197+
uuid := submitOnlineDDL(t, onlineSessionStrategy, alterSQL)
198+
status := onlineddl.WaitForMigrationStatus(
199+
t, &vtParams, shards, uuid, migrationWaitTimeout,
200+
schema.OnlineDDLStatusComplete, schema.OnlineDDLStatusFailed,
201+
)
202+
require.Equal(t, schema.OnlineDDLStatusComplete, status)
203+
assertColumnExists(t, tableName, "d")
204+
})
205+
}
206+
207+
func submitOnlineDDL(t *testing.T, ddlStrategy, sql string) string {
208+
t.Helper()
209+
row := onlineddl.VtgateExecDDL(t, &vtParams, ddlStrategy, sql, "").Named().Row()
210+
require.NotNil(t, row)
211+
uuid := strings.TrimSpace(row.AsString("uuid", ""))
212+
require.NotEmpty(t, uuid)
213+
return uuid
214+
}
215+
216+
func assertTableExists(t *testing.T, tableName string) {
217+
t.Helper()
218+
qr, err := onlineddl.VtgateExecQuery(t.Context(), &vtParams, "show tables like '"+tableName+"'")
219+
require.NoError(t, err)
220+
require.Len(t, qr.Rows, 1, "expected table %s to exist", tableName)
221+
}
222+
223+
func assertColumnExists(t *testing.T, tableName, columnName string) {
224+
t.Helper()
225+
qr, err := onlineddl.VtgateExecQuery(
226+
t.Context(),
227+
&vtParams,
228+
fmt.Sprintf("show columns from %s like '%s'", tableName, columnName),
229+
)
230+
require.NoError(t, err)
231+
require.Len(t, qr.Rows, 1, "expected column %s.%s to exist", tableName, columnName)
232+
}
233+
234+
func assertColumnMissing(t *testing.T, tableName, columnName string) {
235+
t.Helper()
236+
qr, err := onlineddl.VtgateExecQuery(
237+
t.Context(),
238+
&vtParams,
239+
fmt.Sprintf("show columns from %s like '%s'", tableName, columnName),
240+
)
241+
require.NoError(t, err)
242+
require.Empty(t, qr.Rows, "expected column %s.%s to be missing", tableName, columnName)
243+
}

0 commit comments

Comments
 (0)