Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ architecture of that rewrite and the decisions that survive it.

## Rules

- The `ast` package is frozen, and the public `parser` API is stable.
- The `ast` package may only change in backwards-compatible ways: new
node types and new fields are okay; existing nodes, fields, and their
semantics (`Offset`, `Text()`, flags, `Restore` output) must not be
renamed, removed, or changed — sqlc's MySQL engine consumes them.
- The public `parser` API is stable.
- Error messages are part of the contract: `line N column M near "..."`
built from the offending token's recorded position; action errors use
`r.actionErrorf` (reduce-time lookahead position). Failures during
Expand Down
89 changes: 88 additions & 1 deletion ast/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
_ StmtNode = &AlterRangeStmt{}
_ StmtNode = &BeginStmt{}
_ StmtNode = &BinlogStmt{}
_ StmtNode = &ChangeReplicationSourceStmt{}
_ StmtNode = &CommitStmt{}
_ StmtNode = &CreateUserStmt{}
_ StmtNode = &DeallocateStmt{}
Expand Down Expand Up @@ -474,7 +475,10 @@ const (
TrafficOptionReadOnly
)

var _ SensitiveStmtNode = (*TrafficStmt)(nil)
var (
_ SensitiveStmtNode = (*ChangeReplicationSourceStmt)(nil)
_ SensitiveStmtNode = (*TrafficStmt)(nil)
)

// TrafficStmt is traffic operation statement.
type TrafficStmt struct {
Expand Down Expand Up @@ -852,6 +856,89 @@ func (n *BinlogStmt) Accept(v Visitor) (Node, bool) {
return v.Leave(n)
}

// ReplicationSourceOption is a single name = value option of
// ChangeReplicationSourceStmt. Names are stored uppercase; the parser
// does not validate them against the server's option list. Values are
// literals: a string, integer, or decimal.
type ReplicationSourceOption struct {
Name string
Value ValueExpr
}

// Restore implements Node interface.
func (n *ReplicationSourceOption) Restore(ctx *format.RestoreCtx) error {
ctx.WriteKeyWord(n.Name)
ctx.WritePlain(" = ")
if err := n.Value.Restore(ctx); err != nil {
return fmt.Errorf("an error occurred while restore ReplicationSourceOption.Value: %w", err)
}
return nil
}

// ChangeReplicationSourceStmt is a statement to configure a replication
// channel's connection to its source, "CHANGE REPLICATION SOURCE TO"
// (which replaced CHANGE MASTER TO; the removed spelling is not parsed).
// The MySQL 26.7 Change Stream Applier options APPLIER_VERSION,
// APPLIER_WORKER_COUNT, and APPLIER_EVENT_MEMORY_LIMIT parse through the
// same generic option form as the connection options.
// See https://dev.mysql.com/doc/refman/26.7/en/change-replication-source-to.html
type ChangeReplicationSourceStmt struct {
stmtNode

Options []*ReplicationSourceOption
Channel string // FOR CHANNEL clause; empty when absent
}

// Restore implements Node interface.
func (n *ChangeReplicationSourceStmt) Restore(ctx *format.RestoreCtx) error {
ctx.WriteKeyWord("CHANGE REPLICATION SOURCE TO ")
for i, opt := range n.Options {
if i != 0 {
ctx.WritePlain(", ")
}
if err := opt.Restore(ctx); err != nil {
return fmt.Errorf("an error occurred while restore ChangeReplicationSourceStmt.Options[%d]: %w", i, err)
}
}
if n.Channel != "" {
ctx.WriteKeyWord(" FOR CHANNEL ")
ctx.WriteString(n.Channel)
}
return nil
}

// SecureText implements SensitiveStatement interface.
func (n *ChangeReplicationSourceStmt) SecureText() string {
opts := make([]*ReplicationSourceOption, 0, len(n.Options))
for _, opt := range n.Options {
if strings.Contains(opt.Name, "PASSWORD") {
opt = &ReplicationSourceOption{Name: opt.Name, Value: NewValueExpr("xxxxxx", "", "")}
}
opts = append(opts, opt)
}
masked := &ChangeReplicationSourceStmt{Options: opts, Channel: n.Channel}
var sb strings.Builder
_ = masked.Restore(format.NewRestoreCtx(format.DefaultRestoreFlags, &sb))
return sb.String()
}

// Accept implements Node Accept interface.
func (n *ChangeReplicationSourceStmt) Accept(v Visitor) (Node, bool) {
newNode, skipChildren := v.Enter(n)
if skipChildren {
return v.Leave(newNode)
}
n = newNode.(*ChangeReplicationSourceStmt)
for _, opt := range n.Options {
node, ok := opt.Value.Accept(v)
if !ok {
return n, false
}
opt.Value = node.(ValueExpr)
}
return v.Leave(n)
}

// CompletionType defines completion_type used in COMMIT and ROLLBACK statements
type CompletionType int8

Expand Down
7 changes: 7 additions & 0 deletions ast/sem.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ const (
CalibrateResourceCommand = "CALIBRATE RESOURCE"
// CancelDistributionJobCommand represents CANCEL DISTRIBUTION JOB statement
CancelDistributionJobCommand = "CANCEL DISTRIBUTION JOB"
// ChangeReplicationSourceCommand represents CHANGE REPLICATION SOURCE TO statement
ChangeReplicationSourceCommand = "CHANGE REPLICATION SOURCE"
// CommitCommand represents COMMIT statement
CommitCommand = "COMMIT"
// AlterTableCompactCommand represents ALTER TABLE COMPACT statement
Expand Down Expand Up @@ -947,6 +949,11 @@ func (n *BinlogStmt) SEMCommand() string {
return BinlogCommand
}

// SEMCommand returns the command string for the statement.
func (n *ChangeReplicationSourceStmt) SEMCommand() string {
return ChangeReplicationSourceCommand
}

// SEMCommand returns the command string for the statement.
func (n *BRIEStmt) SEMCommand() string {
switch n.Kind {
Expand Down
3 changes: 3 additions & 0 deletions parser/keyword_classes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions parser/keywords.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var Keywords = []KeywordsType{
{"CONVERT", true, "reserved"},
{"CREATE", true, "reserved"},
{"CROSS", true, "reserved"},
{"CUBE", true, "reserved"},
{"CUME_DIST", true, "reserved"},
{"CURRENT_DATE", true, "reserved"},
{"CURRENT_ROLE", true, "reserved"},
Expand Down Expand Up @@ -88,6 +89,7 @@ var Keywords = []KeywordsType{
{"EXISTS", true, "reserved"},
{"EXIT", true, "reserved"},
{"EXPLAIN", true, "reserved"},
{"EXTERNAL", true, "reserved"},
{"FALSE", true, "reserved"},
{"FETCH", true, "reserved"},
{"FIRST_VALUE", true, "reserved"},
Expand Down Expand Up @@ -184,6 +186,7 @@ var Keywords = []KeywordsType{
{"PRECISION", true, "reserved"},
{"PRIMARY", true, "reserved"},
{"PROCEDURE", true, "reserved"},
{"QUALIFY", true, "reserved"},
{"RANGE", true, "reserved"},
{"RANK", true, "reserved"},
{"READ", true, "reserved"},
Expand Down Expand Up @@ -303,6 +306,7 @@ var Keywords = []KeywordsType{
{"CASCADED", false, "unreserved"},
{"CAUSAL", false, "unreserved"},
{"CHAIN", false, "unreserved"},
{"CHANNEL", false, "unreserved"},
{"CHARSET", false, "unreserved"},
{"CHECKPOINT", false, "unreserved"},
{"CHECKSUM", false, "unreserved"},
Expand Down Expand Up @@ -438,6 +442,7 @@ var Keywords = []KeywordsType{
{"LOCATION", false, "unreserved"},
{"LOCKED", false, "unreserved"},
{"LOGS", false, "unreserved"},
{"MANUAL", false, "unreserved"},
{"MASKING", false, "unreserved"},
{"MASTER", false, "unreserved"},
{"MAX_CONNECTIONS_PER_HOUR", false, "unreserved"},
Expand Down Expand Up @@ -490,6 +495,7 @@ var Keywords = []KeywordsType{
{"PAGE_CHECKSUM", false, "unreserved"},
{"PAGE_COMPRESSED", false, "unreserved"},
{"PAGE_COMPRESSION_LEVEL", false, "unreserved"},
{"PARALLEL", false, "unreserved"},
{"PARSER", false, "unreserved"},
{"PARTIAL", false, "unreserved"},
{"PARTITIONING", false, "unreserved"},
Expand Down
8 changes: 4 additions & 4 deletions parser/keywords_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ func TestKeywords(t *testing.T) {
}

func TestKeywordsLength(t *testing.T) {
if !reflect.DeepEqual(679, len(parser.Keywords)) {
t.Fatalf("got %v, want %v", len(parser.Keywords), 679)
if !reflect.DeepEqual(685, len(parser.Keywords)) {
t.Fatalf("got %v, want %v", len(parser.Keywords), 685)
}

reservedNr := 0
Expand All @@ -53,8 +53,8 @@ func TestKeywordsLength(t *testing.T) {
reservedNr += 1
}
}
if !reflect.DeepEqual(233, reservedNr) {
t.Fatalf("got %v, want %v", reservedNr, 233)
if !reflect.DeepEqual(236, reservedNr) {
t.Fatalf("got %v, want %v", reservedNr, 236)
}
}

Expand Down
6 changes: 6 additions & 0 deletions parser/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ var tokenMap = map[string]int{
"CAUSAL": causal,
"CHAIN": chain,
"CHANGE": change,
"CHANNEL": channel,
"CHAR": charType,
"CHARACTER": character,
"CHARSET": charsetKwd,
Expand Down Expand Up @@ -292,6 +293,7 @@ var tokenMap = map[string]int{
"CSV_NULL": csvNull,
"CSV_SEPARATOR": csvSeparator,
"CSV_TRIM_LAST_SEPARATORS": csvTrimLastSeparators,
"CUBE": cube,
"WAIT_TIFLASH_READY": waitTiflashReady,
"WITH_SYS_TABLE": withSysTable,
"IGNORE_STATS": ignoreStats,
Expand Down Expand Up @@ -396,6 +398,7 @@ var tokenMap = map[string]int{
"EXPR_PUSHDOWN_BLACKLIST": exprPushdownBlacklist,
"EXTENDED": extended,
"EXPLORE": explore,
"EXTERNAL": external,
"EXTRACT": extract,
"FALSE": falseKwd,
"FAULTS": faultsSym,
Expand Down Expand Up @@ -538,6 +541,7 @@ var tokenMap = map[string]int{
"LONGBLOB": longblobType,
"LONGTEXT": longtextType,
"LOW_PRIORITY": lowPriority,
"MANUAL": manual,
"MASTER": master,
"MASKING": masking,
"MATCH": match,
Expand Down Expand Up @@ -625,6 +629,7 @@ var tokenMap = map[string]int{
"PAGE_CHECKSUM": pageChecksum,
"PAGE_COMPRESSED": pageCompressed,
"PAGE_COMPRESSION_LEVEL": pageCompressionLevel,
"PARALLEL": parallel,
"PARSER": parser,
"PARTIAL": partial,
"PARTITION": partition,
Expand Down Expand Up @@ -661,6 +666,7 @@ var tokenMap = map[string]int{
"PROFILES": profiles,
"PROXY": proxy,
"PURGE": purge,
"QUALIFY": qualify,
"QUARTER": quarter,
"QUERIES": queries,
"QUERY": query,
Expand Down
72 changes: 72 additions & 0 deletions parser/parse_replication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2026 The sqlc Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package parser

import (
"strings"

"github.com/sqlc-dev/marino/ast"
)

func init() {
rdRegister(change, (*rdParser).parseChangeReplicationSourceStmt)
}

// parseChangeReplicationSourceStmt implements ChangeReplicationSourceStmt.
// The statement postdates the goyacc grammar (parser.y has no production
// for it); the shape follows the MySQL 26.7 reference manual:
//
// "CHANGE" "REPLICATION" "SOURCE" "TO" ReplicationSourceOption
// ("," ReplicationSourceOption)* ("FOR" "CHANNEL" stringLit)?
//
// Option names are not validated against the server's option list, so the
// MySQL 26.7 Change Stream Applier options (APPLIER_VERSION,
// APPLIER_WORKER_COUNT, APPLIER_EVENT_MEMORY_LIMIT) parse like any other.
// The removed CHANGE MASTER TO spelling is not parsed, matching MySQL 8.4+.
func (r *rdParser) parseChangeReplicationSourceStmt() ast.StmtNode {
r.expect(change)
r.expect(replication)
r.expect(source)
r.expect(to)
stmt := &ast.ChangeReplicationSourceStmt{
Options: []*ast.ReplicationSourceOption{r.parseReplicationSourceOption()},
}
for r.accept(int(',')) {
stmt.Options = append(stmt.Options, r.parseReplicationSourceOption())
}
if r.accept(forKwd) {
r.expect(channel)
stmt.Channel = r.expect(stringLit).lit
}
return stmt
}

// parseReplicationSourceOption implements ReplicationSourceOption:
// Identifier eq (stringLit | intLit | decLit | floatLit).
func (r *rdParser) parseReplicationSourceOption() *ast.ReplicationSourceOption {
name := strings.ToUpper(r.parseIdentifier())
r.expect(eq)
var value ast.ValueExpr
switch r.tok() {
case stringLit:
value = ast.NewValueExpr(r.cur().lit, "", "")
r.advance()
case intLit, decLit, floatLit:
value = ast.NewValueExpr(r.cur().item, "", "")
r.advance()
default:
r.syntaxError()
}
return &ast.ReplicationSourceOption{Name: name, Value: value}
}
Loading
Loading