Skip to content

[Bug]: FmtCtx.WriteValue quotes string literals with Go %q syntax — re-serialized SQL breaks or drifts under NO_BACKSLASH_ESCAPES / ANSI_QUOTES #26300

Description

@fengttt

Summary

FmtCtx.WriteValue (pkg/sql/parsers/tree/format.go) formats P_char values under quoteString with Go's %q:

case P_char:
    return ctx.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:

  1. Backslash doubling under NO_BACKSLASH_ESCAPES — value x\y formats as "x\\y"; NBE re-parse yields literal x\\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).

  2. Double quote in value → invalid SQL under NO_BACKSLASH_ESCAPES and ANSI_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.
  3. 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).

Metadata

Metadata

Assignees

Labels

kind/bugSomething isn't workingseverity/s0Active / top priority for current sprint. Owner has committed to working on it now.

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions