You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FmtCtx.WriteValue (pkg/sql/parsers/tree/format.go) formats P_char values under quoteString with Go's %q:
case P_char:
returnctx.WriteString(fmt.Sprintf("%q", v))
%q produces a Go-syntax double-quoted string, not a MySQL string literal. Any statement re-serialized through WithQuoteString(true) and later re-parsed (CTAS follow-up INSERT ... SELECT, index/binder SQL, authenticate paths — see callers below) gets literals that are wrong or invalid under supported sql modes.
This is the generalization of the MATCH-pattern case fixed in #25683: that fix made FullTextMatchExpr mode-aware, but every other string literal still goes through the mode-blind %q path.
Verified failure classes (probe on current main)
Parsed each input under default mode, formatted with WithQuoteString(true), re-parsed under various modes:
Double quote in value → invalid SQL under NO_BACKSLASH_ESCAPESandANSI_QUOTES — value say "hi" formats as "say \"hi\"":
NBE re-parse: syntax error (backslash is not an escape, the \" terminates the string).
ANSI_QUOTES re-parse: syntax error (double quotes delimit identifiers). A CTAS whose source contains such a literal fails outright under these session modes.
Control characters silently change value under NBE — value containing a TAB formats as "tab\there"; NBE re-parse yields literal backslash-t (tab\there), not a TAB.
Additionally (unverified but follows from %q semantics): non-printable bytes are emitted as Go escapes (\x01, é) that MySQL's escaping rules parse differently (\x is not a MySQL escape).
Note ANSI_QUOTES breaks even in the default-session case whenever the session sets it: any%q-quoted literal becomes an identifier reference.
Affected production callers of WithQuoteString(true)
pkg/sql/plan/build_ddl.go:1385,1553 — CTAS CreateAsSelectSql (re-parsed under session mode by the internal executor)
pkg/sql/plan/base_binder.go:2586
pkg/sql/plan/apply_indices_ivfflat.go:368
pkg/frontend/authenticate.go:10998,11182,11633
Suggested fix
Make WriteValue emit a MySQL-syntax single-quoted literal: quote-double ', escape \ per mode (consulting the FmtCtx.noBackslashEscape flag added in #25683), and never emit Go-only escapes. That makes all quoteString serialization mode-correct in one place, and lets FullTextMatchExpr's special-case formatting collapse into the common path.
Care needed: some callers may rely on the current %q output as an opaque key (e.g. semantic/cache keys) rather than re-parseable SQL — those should be audited before changing shared behavior, or the fix gated behind a new option consulted by re-parse-bound callers (CTAS first).
Discovered while reviewing #25683 (see discussion there).
Summary
FmtCtx.WriteValue(pkg/sql/parsers/tree/format.go) formatsP_charvalues underquoteStringwith Go's%q:%qproduces a Go-syntax double-quoted string, not a MySQL string literal. Any statement re-serialized throughWithQuoteString(true)and later re-parsed (CTAS follow-upINSERT ... SELECT, index/binder SQL, authenticate paths — see callers below) gets literals that are wrong or invalid under supported sql modes.This is the generalization of the MATCH-pattern case fixed in #25683: that fix made
FullTextMatchExprmode-aware, but every other string literal still goes through the mode-blind%qpath.Verified failure classes (probe on current main)
Parsed each input under default mode, formatted with
WithQuoteString(true), re-parsed under various modes:Backslash doubling under
NO_BACKSLASH_ESCAPES— valuex\yformats as"x\\y"; NBE re-parse yields literalx\\y(backslashes doubled, silent value drift — same bug class as [Bug] CREATE TABLE AS SELECT with MATCH...AGAINST drops string-literal quotes during rewrite #24823 but for ordinary literals).Double quote in value → invalid SQL under
NO_BACKSLASH_ESCAPESandANSI_QUOTES— valuesay "hi"formats as"say \"hi\"":\"terminates the string).Control characters silently change value under NBE — value containing a TAB formats as
"tab\there"; NBE re-parse yields literal backslash-t (tab\there), not a TAB.Additionally (unverified but follows from
%qsemantics): non-printable bytes are emitted as Go escapes (\x01,é) that MySQL's escaping rules parse differently (\xis not a MySQL escape).Note
ANSI_QUOTESbreaks even in the default-session case whenever the session sets it: any%q-quoted literal becomes an identifier reference.Affected production callers of
WithQuoteString(true)pkg/sql/plan/build_ddl.go:1385,1553— CTASCreateAsSelectSql(re-parsed under session mode by the internal executor)pkg/sql/plan/base_binder.go:2586pkg/sql/plan/apply_indices_ivfflat.go:368pkg/frontend/authenticate.go:10998,11182,11633Suggested fix
Make
WriteValueemit a MySQL-syntax single-quoted literal: quote-double', escape\per mode (consulting theFmtCtx.noBackslashEscapeflag added in #25683), and never emit Go-only escapes. That makes allquoteStringserialization mode-correct in one place, and letsFullTextMatchExpr's special-case formatting collapse into the common path.Care needed: some callers may rely on the current
%qoutput as an opaque key (e.g. semantic/cache keys) rather than re-parseable SQL — those should be audited before changing shared behavior, or the fix gated behind a new option consulted by re-parse-bound callers (CTAS first).Discovered while reviewing #25683 (see discussion there).