Skip to content

vtgate: make sql_mode a session-owned setting - #20880

Open
arthurschreiber wants to merge 18 commits into
arthur/sql-mode-lexer-parser-supportfrom
arthur/vtgate-sql-mode-session-default
Open

vtgate: make sql_mode a session-owned setting#20880
arthurschreiber wants to merge 18 commits into
arthur/sql-mode-lexer-parser-supportfrom
arthur/vtgate-sql-mode-session-default

Conversation

@arthurschreiber

@arthurschreiber arthurschreiber commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

Builds on #20883 (merged) and #20884 (the base branch, in stack #21006), which added MySQL-faithful sql_mode validation and support for the parse-relevant modes. This PR completes the picture: vtgate currently asks an arbitrary shard for @@sql_mode (assuming all backends agree, which nothing guarantees) and runs sessions that never set sql_mode under whatever mode each backend happens to be configured with.

This makes sql_mode a setting the vtgate session owns:

  • A new --sql-mode flag (named after MySQL's, validated the same way as a SET statement) defines the mode every session starts with, defaulting to MySQL's factory default — the mode Vitess-managed mysqld runs with.
  • @@sql_mode / @@global.sql_mode reads and all SET sql_mode handling happen entirely at the vtgate: expressions evaluate through the evalengine with no shard round trips, and the session stores MySQL's canonical form. A dedicated SysVarSQLMode primitive replaces the interim validation shim sql_mode: reject unsupported modes at every layer, neutralize them on every connection #20883 put on the reserved-connection machinery.
  • The session's mode is applied to every query: a SET_VAR(sql_mode = ...) hint on statements that can carry one, and connection settings (the tablet settings pool) for statements that can't (DDL, locks) or setups without SET_VAR. Deployments running --enable-system-settings=false are exempt and keep their backends' modes.
  • Exactly one mode is left out of the transported value: NO_BACKSLASH_ESCAPES (see sql_mode: support the parse-relevant modes at vtgate and vttablet #20884). The session's sql_mode governs how the client's SQL is interpreted, queries sent to vttablet are always vtgate-canonical text, and that text is inert under every mode but the backslash-escaping one. Worth noting that a SET_VAR(sql_mode = ...) hint can never change how its own statement is lexed anyway — MySQL lexes a statement under the session mode in effect before it — so the hint only carries the mode's execution-time semantics, which is exactly what it is for.

Sessions are seeded with the configured default on request entry; the normalizer resolves @@sql_mode (and @@global.sql_mode) to bind variables filled from the session, so SET sql_mode = CONCAT(@@sql_mode, ...) and friends evaluate locally with no shard round trips. A dedicated SysVarSQLMode engine primitive validates, normalizes and stores the canonical value.

Along the way: the SET_VAR comment is now only placed in the top-level statement comment where MySQL honors it (it used to be duplicated into subqueries, which MySQL warns about), explain/vexplain hint the statement they wrap, USE/(V)EXPLAIN no longer force reserved connections, hint ordering is deterministic, and the rendered hint is cached on the session. Review follow-up commits address findings from automated review: plan keys keep discriminating by session sysvars when no hint is injected, undecodable carried session values are repaired on the next SET, and a retried SET sql_mode re-converges open shard sessions on the settings transport.

Measured overhead of always sending the mode: ~5-7µs per query end-to-end (~3µs of that is mysqld parsing/applying the hint), which was ~3% on a loopback-only local cluster and should be well under 1% on real networks.

Related Issue(s)

Part of #20984. Stacked on #20988 and #20884 (stack #21006). Follow-ups: #20892 (gate-side evaluation under the session's mode), #20893 (multi-assignment SET atomicity), #21003 (function-name keyword lexing), #21010 (EXECUTE margin comments), #21014 (the SET_VAR hint on queries the planner derives from the statement), #21015 (a SET commits to the session before the open shard sessions converge).

Checklist

  • "Backport to:" labels have been added if this change should be back-ported to release branches
  • If this change is to be back-ported to previous releases, a justification is included in the PR description (not intended for backport — it's a behavior change)
  • Tests were added or are not required
  • Did the new or modified tests pass consistently locally and on CI? (full unit suites plus the reservedconn end-to-end suites — including mysqldown/servingchange/tabletchange/vttabletdown — pass locally against real clusters; CI pending on this draft)
  • Documentation was added or is not required (changelog summary has three sections covering validation, the session default, and the per-query application)

Decision: no opt-in flag for the session-owned default

The repository's staging rule asks for behavior changes to ship opt-in first and flip a release later. This PR flips the default in one release, deliberately, with --enable-system-settings=false as the opt-out. The reasoning:

  • There is no coherent old behavior to preserve behind a flag. Before this change, SELECT @@sql_mode returned whatever the shard that happened to answer was configured with, and a session that never set sql_mode ran each query under the mode of whichever backend served it. Across keyspaces, or across shards with differing globals, one session got different answers. A flag would keep that as the default for another release without protecting anyone from anything.
  • The new default is MySQL's factory default, the mode Vitess-managed mysqld runs with. Deployments on that default see no change in what their queries run under; what changes is that the vtgate now guarantees it.
  • Deployments that intentionally run a different global have two exact knobs. --sql-mode reproduces their global as the session default; --enable-system-settings=false keeps sql_mode backend-defined wholesale, as before.
  • Mixed-version rollouts are safe in both directions. A seeded session carries its sql_mode in the session proto as a plain system variable, which a v24 vtgate already honors as a session setting, so any session that has touched a v25 vtgate behaves identically on a v24 one. A session that has only seen v24 vtgates behaves as v24 always did. The only divergence window is therefore the pre-existing one.
  • The cost of staging is real and the benefit is not. An opt-in flag now means a deprecated flag next release and its removal the one after, three releases of surface for a default nobody would keep once the new one exists.

The behavior change is called out in the deployment notes and the changelog; this section records that shipping it without an opt-in period was a decision, not an oversight.

Deployment Notes

  • Sessions that never set sql_mode now read the configured default from SELECT @@sql_mode instead of the value of whichever shard happened to answer, and their queries actually run under that mode via SET_VAR hints. Deployments intentionally running a non-default global mode should set --sql-mode accordingly, or set --enable-system-settings=false to keep sql_mode backend-defined.
  • SET sql_mode with a value the backend would reject (or vtgate can't support) now errors — even as a no-op matching the backend's current mode.
  • A SHOW sent to a backend (SHOW CREATE TABLE and friends) runs on a connection carrying the session's settings whenever the session is in a mode that changes its output (ANSI_QUOTES and the other parse-relevant modes, from a SET or from --sql-mode). A session without such a mode sends it on a plain connection, whose output reflects the backend's own configured mode, as in every release so far.
  • Rolling upgrades: session sql_mode values set on an older vtgate are carried across (sql_mode: support the parse-relevant modes at vtgate and vttablet #20884 canonicalizes them on entry and evaluates a carried expression at the vtgate), so in-flight gRPC sessions don't fail mid-upgrade. Only NO_BACKSLASH_ESCAPES is left out of what's sent to the backends.

Details in the changelog summary (changelog/25.0/25.0.0/summary.md).

AI Disclosure

This PR was written primarily by Claude Code — I provided direction and review.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings August 20, 2026 10:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added this to the v25.0.0 milestone Aug 20, 2026
@vitess-bot vitess-bot Bot added NeedsWebsiteDocsUpdate What it says NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsIssue A linked issue is missing for this Pull Request NeedsBackportReason If backport labels have been applied to a PR, a justification is required labels Aug 20, 2026
@vitess-bot

vitess-bot Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Checklist

Hello reviewers! 👋 Please follow this checklist when reviewing this Pull Request.

General

  • Ensure that the Pull Request has a descriptive title.
  • Ensure there is a link to an issue (except for internal cleanup and flaky test fixes), new features should have an RFC that documents use cases and test cases.

Tests

  • Bug fixes should have at least one unit or end-to-end test, enhancement and new features should have a sufficient number of tests.

Documentation

  • Apply the release notes (needs details) label if users need to know about this change.
  • New features should be documented.
  • There should be some code comments as to why things are implemented the way they are.
  • There should be a comment at the top of each new or modified test to explain what the test does.

New flags

  • Is this flag really necessary?
  • Flag names must be clear and intuitive, use dashes (-), and have a clear help text.

If a workflow is added or modified:

  • Each item in Jobs should be named in order to mark it as required.
  • If the workflow needs to be marked as required, the maintainer team must be notified.

Backward compatibility

  • Protobuf changes should be wire-compatible.
  • Changes to _vt tables and RPCs need to be backward compatible.
  • RPC changes should be compatible with vitess-operator
  • If a flag is removed, then it should also be removed from vitess-operator and arewefastyet, if used there.
  • vtctl command output order should be stable and awk-able.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.19651% with 11 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (arthur/sql-mode-lexer-parser-support@6224c02). Learn more about missing BASE report.

Files with missing lines Patch % Lines
go/vt/vtgate/executor.go 92.30% 4 Missing ⚠️
go/vt/vtgate/planbuilder/set.go 88.00% 3 Missing ⚠️
go/vt/vtgate/engine/set.go 94.28% 2 Missing ⚠️
go/vt/vtgate/engine/replace_variables.go 80.00% 1 Missing ⚠️
go/vt/vtgate/vtgate.go 75.00% 1 Missing ⚠️
Additional details and impacted files
@@                           Coverage Diff                           @@
##             arthur/sql-mode-lexer-parser-support   #20880   +/-   ##
=======================================================================
  Coverage                                        ?   81.71%           
=======================================================================
  Files                                           ?      347           
  Lines                                           ?    57557           
  Branches                                        ?        0           
=======================================================================
  Hits                                            ?    47030           
  Misses                                          ?    10527           
  Partials                                        ?        0           
Flag Coverage Δ
partial 81.71% <95.19%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@arthurschreiber arthurschreiber added NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work and removed NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsBackportReason If backport labels have been applied to a PR, a justification is required labels Aug 20, 2026
Copilot AI review requested due to automatic review settings August 20, 2026 13:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 20, 2026 13:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@arthurschreiber arthurschreiber added the Type: Enhancement Logical improvement (somewhere between a bug and feature) label Aug 20, 2026
Copilot AI review requested due to automatic review settings August 20, 2026 17:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings August 20, 2026 18:05
@arthurschreiber
arthurschreiber force-pushed the arthur/vtgate-sql-mode-session-default branch from 5a1a157 to a627f18 Compare August 20, 2026 18:05
@arthurschreiber
arthurschreiber changed the base branch from main to arthur/sql-mode-reject-and-neutralize August 20, 2026 18:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@arthurschreiber arthurschreiber changed the title vtgate: make sql_mode a session-owned setting with MySQL-faithful semantics vtgate: make sql_mode a session-owned setting Aug 20, 2026
Copilot AI review requested due to automatic review settings August 20, 2026 18:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8668d9459b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +905 to +907
if _, ok := session.SystemVariables[sysvars.SQLMode.Name]; !ok {
session.SystemVariables[sysvars.SQLMode.Name] = sqltypes.EncodeStringSQL(defaultSQLMode)
session.setVarCommentBuilt = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the seeded mode to carried reserved connections

During a rolling upgrade, a session from the previous VTGate can have InReservedConn and nonzero shard ReservedIds but no sql_mode; this branch adds the default only to the session map. actionInfo then sees the existing reserved ID and chooses the normal execute path without SetPreQueries, so a first DDL, lock, or other non-hintable statement reuses a connection still running under its old backend mode. Fresh evidence after the explicit-SET convergence fix is that this seeding path still never updates or recreates carried reserved connections.

AGENTS.md reference: AGENTS.md:L240-L240

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. A session carried from an older vtgate with reserved connections and no sql_mode has those connections in the mode that vtgate left them in, the backend's configured one, which is what the deployment note asks --sql-mode to describe; the seed adds that same value to the session. The next SET sql_mode on the session converges them (74dbdeb). Re-creating reserved connections at seed time would drop the locks and temporary tables they hold, which is worse than the window.

// backend, so it does not count. The execution-time modes leave SHOW output alone.
func (vc *VCursorImpl) showOutputFollowsSessionSQLMode() bool {
const showModes = ^sqlparser.SQLModeNoBackslashEscapes
return vc.ParseSQLMode()&showModes != sqlparser.ParseSQLMode(vc.DefaultSQLMode())&showModes

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve backend SHOWs even at the configured default

When the configured/default mode differs from a backend's global mode, this comparison returns false for every fresh session because its mode equals DefaultSQLMode(). A backend-routed SHOW CREATE TABLE therefore bypasses connection settings and formats its result under the backend mode—for example, a backend with ANSI_QUOTES still emits double-quoted identifiers even when VTGate's configured default omits it. Fresh evidence in the final SHOW fix is this default-relative predicate; it does not establish that the backend already has that default, so backend SHOWs requiring session-owned semantics still need the settings transport.

AGENTS.md reference: AGENTS.md:L159-L160

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly addressed in 99bb8d5: the predicate is no longer relative to the configured default, so a session in any SHOW-shaping mode reserves, whatever the backends are configured with. The remaining direction (a backend whose global mode carries such a mode while the session does not) keeps the behaviour of every release so far: the SHOW runs on a plain connection and prints the backend's own formatting. Reserving every backend SHOW would pin a connection for each session that runs SHOW CREATE TABLE, and the deployment note already asks operators to describe the backends' mode with --sql-mode; the description now says so under the deployment notes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to the note above: a SHOW that reserves does not pin a MySQL connection. vtgate flips the session into the settings transport, and vttablet serves the statement from its settings pool (ReserveExecute tries executeWithSettings first and only truly reserves when the statement demands it, which a SHOW never does). The cost is that the session flag is sticky: every later query of that session goes through the reserve RPC with pre-queries and the settings pool instead of the plain pool with a SET_VAR hint. Making the settings transport per statement for hint-incapable statements would make it cheap to send every backend SHOW with the session's settings; that is the direction being considered.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-statement settings transport is now #21016, against main. Once it lands, the rule here can become "every backend SHOW carries the session's settings", which closes the remaining direction of this thread.

arthurschreiber and others added 10 commits September 4, 2026 15:21
…antics

Builds on the sql_mode rejection layer: vtgate now owns the session's
sql_mode instead of asking an arbitrary shard and hoping all backends
agree.

- A new --sql-mode flag (named after MySQL's, validated like a SET
  statement) defines the mode every session starts with, defaulting to
  MySQL's factory default. Deployments with --enable-system-settings
  disabled are exempt and keep their backends' modes.
- @@sql_mode and @@global.sql_mode reads and all SET sql_mode handling
  happen entirely at the vtgate with no shard round trips: expressions
  evaluate through the evalengine, values are stored in MySQL's
  canonical form, and a dedicated SysVarSQLMode primitive replaces the
  interim validation shim on the reserved-connection machinery.
- The session's mode is applied to every query: a SET_VAR hint on
  statements that can carry one, connection settings for statements that
  cannot (DDL, locks) and setups without SET_VAR support. The SET_VAR
  comment is rendered once and cached on the session, placed only in the
  top-level statement where MySQL honors it, and never carries lexer
  modes; plan cache keys keep discriminating by session variables even
  when no hint is injected.
- Session state carried from older vtgates is grandfathered: validation
  applies to new SET statements, undecodable stored values are repaired
  on the next SET, and a retried SET converges open shard sessions on
  the settings transport.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
CI fixes for the always-sent sql_mode: expectations that assert exact
query text now account for the SET_VAR(sql_mode = ...) hint or strip
optimizer hints before comparing, the twopc redo-statement goldens
carry the combined settings statement, and the schemadiff vrepl suite
sets --sql-mode so its zero-date table definitions remain valid under
the vtgate-enforced session mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
MySQL's DEFAULT restores the backend's global sql_mode, which is
per-keyspace and incoherent under scatter. The session-owned sql_mode
defines DEFAULT as the configured default the session started with: the
value of the --sql-mode flag, or the compiled-in default when the flag
is unset. The planner substitutes the DEFAULT keyword with a string
literal of that value, so it flows through the same evaluation,
validation, and canonical storage as any other assignment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
Editorial only, no facts added or removed: the session-default and
always-send sections carry one idea per sentence now, with the long
clause chains split up — easier reading for operators skimming release
notes, non-native English readers included. The sections inherited from
the base branch are left as they are: the base branch carries their
rewritten versions, and this branch picks those up on its next rebase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…g help

The session-owned sql_mode forwards every mode but NO_BACKSLASH_ESCAPES to the
backends, the rule the parse-relevant mode support established; the always-sent
section and the --sql-mode help still described the earlier strip of every
parse-relevant mode.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hkhyn4HQQfBbJkmyPe2Gtm
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…e error value

A table test verified against MySQL 8.0.46 covers the odder expressions a SET
sql_mode can carry: other session variables, user variables, subqueries, RAND(),
literals of every kind, and the error cases. Expressions VTGate can compute are
evaluated locally; a sub-expression it cannot, such as RAND() or a system variable the
session never set, is fetched from a shard with the session's @@sql_mode passed along
and the result validated like any other value. The changelog said no shard round trip
at all.

ER_WRONG_VALUE_FOR_VAR now carries the offending value truncated to 200 characters,
as MySQL's does, instead of embedding an arbitrarily long expression result.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hkhyn4HQQfBbJkmyPe2Gtm
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…comment only

Review feedback: with every query carrying the session's sql_mode, a statement with
two optimizer hint comments failed with "Must have only one query hint" where it used
to go through. MySQL honors only the first hint comment of a statement and reads any
later one as an ordinary comment, so the session's setting now joins the first and the
rest are kept as they are.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
MySQL resolves every expression of a SET before applying any assignment, verified on
8.0.46; the once-per-request @@sql_mode bind variable gives exactly that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
Review feedback: SHOW statements were exempt from reserving a connection, on the
grounds that vtgate answers them itself. The ones it sends to a backend — SHOW CREATE
TABLE, SHOW TABLES, SHOW COLUMNS, ... — cannot carry a SET_VAR hint, and their output
depends on the mode: SHOW CREATE TABLE quotes identifiers by it. Only a SHOW whose
plan is local stays exempt; one sent to a backend now runs on a connection with the
session's settings applied.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…holds

Review feedback: with SET_VAR available the SET stored the value and stopped, since
every hint-capable query carries the session's sql_mode itself. A reserved connection
the session already holds — after a DDL, a lock, or any other statement that could
not carry a hint — kept the mode it was set up with and served the next such
statement under it. Those connections are now updated whether or not SET_VAR is in
use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
Copilot AI review requested due to automatic review settings September 4, 2026 15:23
@arthurschreiber
arthurschreiber force-pushed the arthur/vtgate-sql-mode-session-default branch from 8668d94 to 74dbdeb Compare September 4, 2026 15:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74dbdeb953

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// The reserved connections the session already holds — after a DDL, a lock, or
// any other statement that could not carry a hint — keep the mode they were set
// up with and serve the next such statement, so they are updated, SET_VAR or not.
rss := vcursor.Session().ShardSession()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update reserved PRE and POST shard sessions

When SET_VAR is unavailable and a transaction has created reserved lookup-vindex connections under PreSessions or PostSessions, this call only returns Session.ShardSessions, so those connections retain the previous sql_mode; later PRE/POST vindex operations reuse them without a hint and execute under stale semantics. Fresh evidence after the earlier convergence fix is that VCursorImpl.ShardSession() reads only the normal commit-order list and does not select all shard sessions whose ReservedId is nonzero.

AGENTS.md reference: AGENTS.md:L159-L160

Useful? React with 👍 / 👎.

arthurschreiber and others added 8 commits September 4, 2026 15:36
…able is removed

SQLMode looked the session up twice in a row. RemoveSystemVariable now
invalidates the cached SET_VAR comment the way SetSystemVariable does, so a
later render cannot hand out a hint that still names the removed variable.
The vexplain end-to-end check reports through testify like the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
… SHOW never does

The rule compared the session's parse-relevant modes with the configured
default's, so a default that carries ANSI_QUOTES sent SHOW CREATE TABLE on a
plain connection and got backticks back. The session's own value decides
now: any parse-relevant mode other than NO_BACKSLASH_ESCAPES reserves. A
SHOW answered from the topology (SHOW VITESS_METADATA VARIABLES) never
reaches a tablet and is treated like one vtgate answers itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…SLASH_ESCAPES

The SET that updates the reserved shard sessions a session holds sent the
full value; the hint and the settings transports already leave the one mode
out that a backend must not lex under. It is stripped here too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
… sql_mode

Both happen before the first statement reaches the executor and seeds the
session: a multi-statement request is split by the session's parser, which
fell back to the default-mode parser when the session carried no sql_mode
yet, and the handshake advertised backslash escaping whatever --sql-mode
said. The splitter now reads the configured default, and NewConnection sets
the NO_BACKSLASH_ESCAPES status flag when the default carries it, unless the
deployment leaves the sql_mode to the backends.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
DefaultSQLMode substituted the compiled-in modes when the configured value
was empty, so sessions seeded with the empty mode saw SET sql_mode = DEFAULT
enable modes they never started with. The executor resolves the flag once;
the vcursor now returns that value as it is.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…he sql_mode to them

With --enable-system-settings=false the session is never seeded, but the
normalizer still answered @@sql_mode, @@global.sql_mode and the sql_mode row
of SHOW VARIABLES with vtgate's configured default, which says nothing about
the mode such a deployment's queries run under. The rewrite now happens only
when the session carries a sql_mode, which every session vtgate manages the
sql_mode of does from its first request on; otherwise the read goes to the
backend as written.

Materialization of a carried expression puts the session in the configured
default before evaluating, rather than removing the variable, so @@sql_mode
inside the expression keeps resolving to the default the session started
with. The plan tests normalize with a seeded session, like a real one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
Both scopes of SHOW VARIABLES filled the sql_mode row from the session,
while @@global.sql_mode already read the configured default. The global
form now carries its own bind variable for the row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…nner

The evaluation translated the expression directly, so a sub-expression
vtgate cannot compute (a backend function, an unset system variable) failed
the request and dropped the carried value. The expression now runs as the
SET sql_mode statement that stored it, through the planner, which fetches
such a sub-expression from a shard the way it does for any SET.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
Copilot AI review requested due to automatic review settings September 4, 2026 15:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a89c4491dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// a default that carries such a mode is not known to be what the backends run under.
func (vc *VCursorImpl) showOutputFollowsSessionSQLMode() bool {
const showModes = ^sqlparser.SQLModeNoBackslashEscapes
return vc.ParseSQLMode()&showModes != 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply an empty session mode to backend SHOWs

When the session's parse mode is empty but a backend's global mode contains ANSI_QUOTES, this predicate is false, so a backend-routed SHOW CREATE TABLE runs without connection settings and emits double-quoted identifiers despite VTGate owning and reporting the empty mode. Fresh evidence after the claimed SHOW fix is that the final predicate still tests only whether the session has a nonzero parse bit, not whether the backend may have a bit that the session needs cleared; backend SHOWs must therefore apply the transported mode even in this direction.

AGENTS.md reference: AGENTS.md:L159-L160

Useful? React with 👍 / 👎.

Comment thread go/vt/vtgate/executor.go
Comment on lines +1536 to +1537
safeSession.SetSystemVariable(sysvars.SQLMode.Name, sqltypes.EncodeStringSQL(e.config.SQLMode))
err := e.evaluateSessionSQLMode(ctx, safeSession, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the opt-out when materializing carried sql_mode

When a mixed-version session carries an expression-form sql_mode and has EnableSystemSettings=false, this installs the configured default before replaying the expression through the SET planner; buildSetOpSQLMode then takes its disabled-settings check-and-ignore path, returns success without storing the evaluated value, and this function leaves the injected default in the session. Fresh evidence after the opt-out rewrite is this successful ignored-SET path, which makes subsequent parsing, reads, and hints use VTGate's default instead of retaining backend ownership; skip materialization for such sessions or restore/drop the carried value explicitly when the planner ignores it.

AGENTS.md reference: AGENTS.md:L240-L240

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Component: Documentation docs related issues/PRs Component: vtexplain changes made to vtexplain code Component: VTGate Component: VTTablet NeedsDescriptionUpdate The description is not clear or comprehensive enough, and needs work NeedsIssue A linked issue is missing for this Pull Request NeedsWebsiteDocsUpdate What it says Type: Enhancement Logical improvement (somewhere between a bug and feature)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants