vitessio/vitess · error
[BUG] tried to replace 'SQLNode' on 'RootNode'
Error message
[BUG] tried to replace 'SQLNode' on 'RootNode'
What it means
In the generated AST rewriter, RootNode.SQLNode is the root placeholder that cannot be replaced: the generated rewrite callback unconditionally panics if a rewrite attempts to swap it. This is a defensive invariant — rewrite functions must never return a replacement for the root wrapper itself.
Source
Thrown at go/vt/sqlparser/ast_rewrite.go:11117
func (a *application) rewriteRootNode(parent SQLNode, node RootNode, replacer replacerFunc) bool {
if a.pre != nil {
a.cur.replacer = replacer
a.cur.parent = parent
a.cur.node = node
kontinue := !a.pre(&a.cur)
if a.cur.revisit {
a.cur.revisit = false
return a.rewriteSQLNode(parent, a.cur.node, replacer)
}
if kontinue {
return true
}
}
if a.collectPaths {
a.cur.current.AddStep(uint16(RootNodeSQLNode))
}
if !a.rewriteSQLNode(node, node.SQLNode, func(newNode, parent SQLNode) {
panic("[BUG] tried to replace 'SQLNode' on 'RootNode'")
}) {
return false
}
if a.collectPaths {
a.cur.current.Pop()
}
if a.post != nil {
a.cur.replacer = replacer
a.cur.parent = parent
a.cur.node = node
if !a.post(&a.cur) {
return false
}
}
return true
}
// Function Generation Source: PtrToStructMethodView on GitHub (pinned to 01a25a7d17)
Solutions
- In your rewrite callback, only return a replacement for the specific node kinds you intend to change — never for the RootNode wrapper
- Add a type/parent check: skip replacement when parent is the root (e.g. only replace *sqlparser.TableName, *ColName, etc.)
- Return (node, false) (or the original node) from the callback for nodes you do not want to alter
- Regenerate with `make codegen` if you forked ast_rewrite.go and the invariant drifted
Example fix
// before
func repl(node, parent sqlparser.SQLNode) (sqlparser.SQLNode, bool) {
return newExpr, true // replaces everything, incl. root
}
// after
func repl(node, parent sqlparser.SQLNode) (sqlparser.SQLNode, bool) {
if _, ok := node.(*sqlparser.TableName); !ok {
return node, false
}
return newExpr, true
} Defensive patterns
Strategy: try-catch
Validate before calling
func repl(node, parent sqlparser.SQLNode) (sqlparser.SQLNode, bool) {
// never replace when the node is the root wrapper
if node == nil {
return node, false
}
switch node.(type) {
case *sqlparser.TableName, *sqlparser.ColName:
return replaceIfMatched(node)
default:
return node, false
}
} Try / catch
err := func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("rewrite panicked: %v", r)
}
}()
return sqlparser.SafeRewrite(stmt, opts, repl)
}() Prevention
- Only return replacements for concrete node types you target
- Never return (newNode, true) unconditionally from a rewrite callback
- Skip identifier and root wrapper nodes in callbacks
- Test rewriters against full statements, not isolated expressions
When it happens
Trigger: Supplying a Rewrite function (or using RewriteOptions) that returns a non-nil replacement when invoked with parent=RootNode, e.g. an unconditional `return newExpr, true` in the callback instead of only replacing targeted child nodes.
Common situations: Custom rewriters that replace nodes without checking the parent type; rewriters that match on the root SQLNode wrapper produced by safeRewrite; forked generated code after a parser regen.
Related errors
- [BUG] tried to replace 'Name' on 'TableName'
- [BUG] tried to replace 'Qualifier' on 'TableName'
- [BUG] tried to replace 'Key' on 'VindexParam'
- [BUG] tried to replace 'VisitThis' on 'Visitable'
- [BUG] tried to replace 'ASTType' on 'ValueContainer'
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/717f3216876b75df.
Report an issue: GitHub.