From 0d04868ec0ca0bca12a99476c6a5559f75bfff88 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 21:03:38 +0000 Subject: [PATCH 1/4] Support the MySQL XA transaction and instance backup lock statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_txn statement coverage group (MySQL 26.7 §15.3.5 and §15.3.8): its error goldens turn into Restore() goldens and the group is renamed to mysql_txn with expanded coverage, following the mysql_admin precedent. Grammar, written from the reference manual since these statements postdate the goyacc grammar (parser/parse_xa.go): - XA {START|BEGIN} xid [JOIN|RESUME], XA END xid [SUSPEND [FOR MIGRATE]], XA PREPARE xid, XA COMMIT xid [ONE PHASE], XA ROLLBACK xid, and XA RECOVER [CONVERT XID], with xid: gtrid [, bqual [, formatID]]. The XA BEGIN spelling parses to XAOpStart, so Restore() canonicalizes it to XA START. - LOCK INSTANCE FOR BACKUP and UNLOCK INSTANCE join the LOCK/UNLOCK statement families in parse_misc.go. New AST nodes (ast/xa.go): XAStmt with the XID transaction identifier node, LockInstanceStmt, and UnlockInstanceStmt, plus their SEMCommand strings. Keyword tables: MIGRATE, ONE, PHASE, SUSPEND, XA, and XID become unreserved keywords, matching their MySQL 26.7 classification; TestKeywordsLength counts updated accordingly. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FGwevXQRPY7iPxud2UyaJH --- ast/sem.go | 45 ++++ ast/xa.go | 221 ++++++++++++++++++ parser/keyword_classes.go | 6 + parser/keywords.go | 6 + parser/keywords_test.go | 4 +- parser/misc.go | 6 + parser/parse_misc.go | 19 +- parser/parse_xa.go | 107 +++++++++ parser/testdata/parser/mysql_txn/input.sql | 37 +++ parser/testdata/parser/mysql_txn/output.sql | 37 +++ .../parser/mysql_unsupported_txn/input.sql | 17 -- .../parser/mysql_unsupported_txn/output.sql | 17 -- parser/token_kinds.go | 6 + 13 files changed, 490 insertions(+), 38 deletions(-) create mode 100644 ast/xa.go create mode 100644 parser/parse_xa.go create mode 100644 parser/testdata/parser/mysql_txn/input.sql create mode 100644 parser/testdata/parser/mysql_txn/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_txn/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_txn/output.sql diff --git a/ast/sem.go b/ast/sem.go index c7e8103..8230767 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -526,6 +526,22 @@ const ( LoadIndexCommand = "LOAD INDEX INTO CACHE" // ResetPersistCommand represents RESET PERSIST statement ResetPersistCommand = "RESET PERSIST" + // XAStartCommand represents XA START statement + XAStartCommand = "XA START" + // XAEndCommand represents XA END statement + XAEndCommand = "XA END" + // XAPrepareCommand represents XA PREPARE statement + XAPrepareCommand = "XA PREPARE" + // XACommitCommand represents XA COMMIT statement + XACommitCommand = "XA COMMIT" + // XARollbackCommand represents XA ROLLBACK statement + XARollbackCommand = "XA ROLLBACK" + // XARecoverCommand represents XA RECOVER statement + XARecoverCommand = "XA RECOVER" + // LockInstanceCommand represents LOCK INSTANCE FOR BACKUP statement + LockInstanceCommand = "LOCK INSTANCE" + // UnlockInstanceCommand represents UNLOCK INSTANCE statement + UnlockInstanceCommand = "UNLOCK INSTANCE" // UnknownCommand represents unknown statements UnknownCommand = "UNKNOWN" // SetOprCommand represents UNION/INTERSECT/EXCEPT statement @@ -1502,3 +1518,32 @@ func (n *LoadIndexStmt) SEMCommand() string { func (n *ResetPersistStmt) SEMCommand() string { return ResetPersistCommand } + +// SEMCommand returns the command string for the statement. +func (n *XAStmt) SEMCommand() string { + switch n.Op { + case XAOpStart: + return XAStartCommand + case XAOpEnd: + return XAEndCommand + case XAOpPrepare: + return XAPrepareCommand + case XAOpCommit: + return XACommitCommand + case XAOpRollback: + return XARollbackCommand + case XAOpRecover: + return XARecoverCommand + } + return UnknownCommand +} + +// SEMCommand returns the command string for the statement. +func (n *LockInstanceStmt) SEMCommand() string { + return LockInstanceCommand +} + +// SEMCommand returns the command string for the statement. +func (n *UnlockInstanceStmt) SEMCommand() string { + return UnlockInstanceCommand +} diff --git a/ast/xa.go b/ast/xa.go new file mode 100644 index 0000000..eaf0455 --- /dev/null +++ b/ast/xa.go @@ -0,0 +1,221 @@ +// 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 ast + +import ( + "github.com/sqlc-dev/marino/format" +) + +// The MySQL XA transaction statements (MySQL 26.7 §15.3.8) and the +// backup lock statements LOCK INSTANCE FOR BACKUP / UNLOCK INSTANCE +// (§15.3.5). + +var ( + _ Node = &XID{} + + _ StmtNode = &XAStmt{} + _ StmtNode = &LockInstanceStmt{} + _ StmtNode = &UnlockInstanceStmt{} +) + +// XID is the transaction identifier of an XA statement: +// xid: gtrid [, bqual [, formatID]]. +// Gtrid and Bqual are string literals; HasBqual distinguishes an absent +// bqual from an empty one, and HasFormatID requires HasBqual. +type XID struct { + node + + Gtrid string + Bqual string + HasBqual bool + FormatID uint64 + HasFormatID bool +} + +// Restore implements Node interface. +func (n *XID) Restore(ctx *format.RestoreCtx) error { + ctx.WriteString(n.Gtrid) + if n.HasBqual { + ctx.WritePlain(", ") + ctx.WriteString(n.Bqual) + if n.HasFormatID { + ctx.WritePlainf(", %d", n.FormatID) + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *XID) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*XID) + return v.Leave(n) +} + +// XAOp is the operation of an XAStmt. +type XAOp int + +const ( + // XAOpStart is XA START (the XA BEGIN spelling parses to it too). + XAOpStart XAOp = iota + // XAOpEnd is XA END. + XAOpEnd + // XAOpPrepare is XA PREPARE. + XAOpPrepare + // XAOpCommit is XA COMMIT. + XAOpCommit + // XAOpRollback is XA ROLLBACK. + XAOpRollback + // XAOpRecover is XA RECOVER. + XAOpRecover +) + +// XAStartOption is the JOIN/RESUME modifier of XA START. +type XAStartOption int + +const ( + // XAStartNone omits the modifier. + XAStartNone XAStartOption = iota + // XAStartJoin is XA START ... JOIN. + XAStartJoin + // XAStartResume is XA START ... RESUME. + XAStartResume +) + +// XAStmt is an XA transaction statement: +// +// XA {START|BEGIN} xid [JOIN|RESUME] +// XA END xid [SUSPEND [FOR MIGRATE]] +// XA PREPARE xid +// XA COMMIT xid [ONE PHASE] +// XA ROLLBACK xid +// XA RECOVER [CONVERT XID] +// +// Xid is nil for XA RECOVER only. ForMigrate requires Suspend; both +// belong to XA END. OnePhase belongs to XA COMMIT and ConvertXID to +// XA RECOVER. +type XAStmt struct { + stmtNode + + Op XAOp + Xid *XID + StartOption XAStartOption + Suspend bool + ForMigrate bool + OnePhase bool + ConvertXID bool +} + +// Restore implements Node interface. +func (n *XAStmt) Restore(ctx *format.RestoreCtx) error { + switch n.Op { + case XAOpStart: + ctx.WriteKeyWord("XA START ") + case XAOpEnd: + ctx.WriteKeyWord("XA END ") + case XAOpPrepare: + ctx.WriteKeyWord("XA PREPARE ") + case XAOpCommit: + ctx.WriteKeyWord("XA COMMIT ") + case XAOpRollback: + ctx.WriteKeyWord("XA ROLLBACK ") + case XAOpRecover: + ctx.WriteKeyWord("XA RECOVER") + if n.ConvertXID { + ctx.WriteKeyWord(" CONVERT XID") + } + return nil + } + if err := n.Xid.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore XAStmt.Xid") + } + switch n.StartOption { + case XAStartJoin: + ctx.WriteKeyWord(" JOIN") + case XAStartResume: + ctx.WriteKeyWord(" RESUME") + } + if n.Suspend { + ctx.WriteKeyWord(" SUSPEND") + if n.ForMigrate { + ctx.WriteKeyWord(" FOR MIGRATE") + } + } + if n.OnePhase { + ctx.WriteKeyWord(" ONE PHASE") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *XAStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*XAStmt) + if n.Xid != nil { + node, ok := n.Xid.Accept(v) + if !ok { + return n, false + } + n.Xid = node.(*XID) + } + return v.Leave(n) +} + +// LockInstanceStmt is a LOCK INSTANCE FOR BACKUP statement. +type LockInstanceStmt struct { + stmtNode +} + +// Restore implements Node interface. +func (n *LockInstanceStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("LOCK INSTANCE FOR BACKUP") + return nil +} + +// Accept implements Node Accept interface. +func (n *LockInstanceStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*LockInstanceStmt) + return v.Leave(n) +} + +// UnlockInstanceStmt is an UNLOCK INSTANCE statement. +type UnlockInstanceStmt struct { + stmtNode +} + +// Restore implements Node interface. +func (n *UnlockInstanceStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("UNLOCK INSTANCE") + return nil +} + +// Accept implements Node Accept interface. +func (n *UnlockInstanceStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*UnlockInstanceStmt) + return v.Leave(n) +} diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index d6ffd7b..2c73b1d 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -430,6 +430,12 @@ var unReservedKeywordNames = []string{ "UPGRADE", "USE_FRM", "VCPU", + "MIGRATE", + "ONE", + "PHASE", + "SUSPEND", + "XA", + "XID", "CODE", "LIBRARY", "MUTEX", diff --git a/parser/keywords.go b/parser/keywords.go index b07f399..fb3ca02 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -470,6 +470,7 @@ var Keywords = []KeywordsType{ {"MEMORY", false, "unreserved"}, {"MERGE", false, "unreserved"}, {"MICROSECOND", false, "unreserved"}, + {"MIGRATE", false, "unreserved"}, {"MINUTE", false, "unreserved"}, {"MINVALUE", false, "unreserved"}, {"MIN_ROWS", false, "unreserved"}, @@ -499,6 +500,7 @@ var Keywords = []KeywordsType{ {"OLTP_READ_ONLY", false, "unreserved"}, {"OLTP_READ_WRITE", false, "unreserved"}, {"OLTP_WRITE_ONLY", false, "unreserved"}, + {"ONE", false, "unreserved"}, {"ONLINE", false, "unreserved"}, {"ONLY", false, "unreserved"}, {"ON_DUPLICATE", false, "unreserved"}, @@ -522,6 +524,7 @@ var Keywords = []KeywordsType{ {"PERSIST", false, "unreserved"}, {"PER_DB", false, "unreserved"}, {"PER_TABLE", false, "unreserved"}, + {"PHASE", false, "unreserved"}, {"PLUGIN", false, "unreserved"}, {"PLUGINS", false, "unreserved"}, {"POINT", false, "unreserved"}, @@ -632,6 +635,7 @@ var Keywords = []KeywordsType{ {"SUBPARTITION", false, "unreserved"}, {"SUBPARTITIONS", false, "unreserved"}, {"SUPER", false, "unreserved"}, + {"SUSPEND", false, "unreserved"}, {"SWAPS", false, "unreserved"}, {"SWITCHES", false, "unreserved"}, {"SYSTEM", false, "unreserved"}, @@ -688,6 +692,8 @@ var Keywords = []KeywordsType{ {"WITH_SYS_TABLE", false, "unreserved"}, {"WORKLOAD", false, "unreserved"}, {"X509", false, "unreserved"}, + {"XA", false, "unreserved"}, + {"XID", false, "unreserved"}, {"YEAR", false, "unreserved"}, {"ADMIN", false, "tidb"}, {"BATCH", false, "tidb"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 4bb3652..9636bf4 100644 --- a/parser/keywords_test.go +++ b/parser/keywords_test.go @@ -43,8 +43,8 @@ func TestKeywords(t *testing.T) { } func TestKeywordsLength(t *testing.T) { - if !reflect.DeepEqual(712, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 712) + if !reflect.DeepEqual(718, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 718) } reservedNr := 0 diff --git a/parser/misc.go b/parser/misc.go index de90833..9a71a20 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -575,6 +575,7 @@ var tokenMap = map[string]int{ "METADATA": metadata, "MICROSECOND": microsecond, "MIDDLEINT": middleIntType, + "MIGRATE": migrate, "MIN_ROWS": minRows, "MIN": min, "MINUTE_MICROSECOND": minuteMicrosecond, @@ -622,6 +623,7 @@ var tokenMap = map[string]int{ "TPCH_10": tpch10, "ON_DUPLICATE": onDuplicate, "ON": on, + "ONE": one, "ONLINE": online, "ONLY": only, "OPEN": open, @@ -655,6 +657,7 @@ var tokenMap = map[string]int{ "PER_TABLE": per_table, "PERSIST": persist, "PESSIMISTIC": pessimistic, + "PHASE": phase, "PLACEMENT": placement, "PLAN": plan, "PLAN_CACHE": planCache, @@ -858,6 +861,7 @@ var tokenMap = map[string]int{ "SUM": sum, "SUPER": super, "SURVIVAL_PREFERENCES": survivalPreferences, + "SUSPEND": suspend, "SWAPS": swaps, "SWITCHES": switchesSym, "SWITCH_GROUP": switchGroup, @@ -981,6 +985,8 @@ var tokenMap = map[string]int{ "WRITE": write, "WORKLOAD": workload, "X509": x509, + "XA": xa, + "XID": xid, "XOR": xor, "YEAR_MONTH": yearMonth, "YEAR": yearType, diff --git a/parser/parse_misc.go b/parser/parse_misc.go index 14041cb..a57cf01 100644 --- a/parser/parse_misc.go +++ b/parser/parse_misc.go @@ -321,11 +321,19 @@ func (r *rdParser) parseRestartStmt() ast.StmtNode { } // parseLockStmtFamily dispatches LOCK-leading statements: LockTablesStmt -// here, LockStatsStmt in parse_tidb.go. +// and LockInstanceStmt here, LockStatsStmt in parse_tidb.go. func (r *rdParser) parseLockStmtFamily() ast.StmtNode { if r.la(1) == stats { return r.parseLockStatsStmt() } + if r.la(1) == instance { + // LockInstanceStmt: "LOCK" "INSTANCE" "FOR" "BACKUP" + r.advance() + r.advance() + r.expect(forKwd) + r.expect(backup) + return &ast.LockInstanceStmt{} + } // LockTablesStmt: "LOCK" TablesTerminalSym TableLockList r.expect(lock) if !r.accept(tables) && !r.accept(tableKwd) { @@ -367,11 +375,18 @@ func (r *rdParser) parseTableLock() ast.TableLock { } // parseUnlockStmtFamily dispatches UNLOCK-leading statements: -// UnlockTablesStmt here, UnlockStatsStmt in parse_tidb.go. +// UnlockTablesStmt and UnlockInstanceStmt here, UnlockStatsStmt in +// parse_tidb.go. func (r *rdParser) parseUnlockStmtFamily() ast.StmtNode { if r.la(1) == stats { return r.parseUnlockStatsStmt() } + if r.la(1) == instance { + // UnlockInstanceStmt: "UNLOCK" "INSTANCE" + r.advance() + r.advance() + return &ast.UnlockInstanceStmt{} + } // UnlockTablesStmt: "UNLOCK" TablesTerminalSym r.expect(unlock) if !r.accept(tables) && !r.accept(tableKwd) { diff --git a/parser/parse_xa.go b/parser/parse_xa.go new file mode 100644 index 0000000..63053bd --- /dev/null +++ b/parser/parse_xa.go @@ -0,0 +1,107 @@ +// 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 + +// The MySQL XA transaction statements (MySQL 26.7 §15.3.8). These +// postdate the goyacc grammar; the productions below are written from +// the MySQL 26.7 reference manual in the same style as parser.y. (The +// LOCK INSTANCE FOR BACKUP / UNLOCK INSTANCE statements of §15.3.5 are +// alternatives of the LOCK/UNLOCK statement families in parse_misc.go.) + +import ( + "github.com/sqlc-dev/marino/ast" +) + +func init() { + rdRegister(xa, (*rdParser).parseXAStmt) +} + +// parseXAStmt implements XAStmt: +// +// "XA" ("START" | "BEGIN") XID ["JOIN" | "RESUME"] +// | "XA" "END" XID ["SUSPEND" ["FOR" "MIGRATE"]] +// | "XA" "PREPARE" XID +// | "XA" "COMMIT" XID ["ONE" "PHASE"] +// | "XA" "ROLLBACK" XID +// | "XA" "RECOVER" ["CONVERT" "XID"] +// +// The XA BEGIN spelling parses to XAOpStart, so Restore() canonicalizes +// it to XA START. +func (r *rdParser) parseXAStmt() ast.StmtNode { + r.expect(xa) + switch r.tok() { + case start, begin: + r.advance() + stmt := &ast.XAStmt{Op: ast.XAOpStart, Xid: r.parseXID()} + switch r.tok() { + case join: + r.advance() + stmt.StartOption = ast.XAStartJoin + case resume: + r.advance() + stmt.StartOption = ast.XAStartResume + } + return stmt + case end: + r.advance() + stmt := &ast.XAStmt{Op: ast.XAOpEnd, Xid: r.parseXID()} + if r.accept(suspend) { + stmt.Suspend = true + if r.accept(forKwd) { + r.expect(migrate) + stmt.ForMigrate = true + } + } + return stmt + case prepare: + r.advance() + return &ast.XAStmt{Op: ast.XAOpPrepare, Xid: r.parseXID()} + case commit: + r.advance() + stmt := &ast.XAStmt{Op: ast.XAOpCommit, Xid: r.parseXID()} + if r.accept(one) { + r.expect(phase) + stmt.OnePhase = true + } + return stmt + case rollback: + r.advance() + return &ast.XAStmt{Op: ast.XAOpRollback, Xid: r.parseXID()} + case recover: + r.advance() + stmt := &ast.XAStmt{Op: ast.XAOpRecover} + if r.accept(convert) { + r.expect(xid) + stmt.ConvertXID = true + } + return stmt + } + r.syntaxError() + return nil +} + +// parseXID implements the xid production: +// stringLit [',' stringLit [',' NUM]]. +func (r *rdParser) parseXID() *ast.XID { + x := &ast.XID{Gtrid: r.expect(stringLit).lit} + if r.accept(int(',')) { + x.Bqual = r.expect(stringLit).lit + x.HasBqual = true + if r.accept(int(',')) { + x.FormatID = getUint64FromNUM(r.expect(intLit).item) + x.HasFormatID = true + } + } + return x +} diff --git a/parser/testdata/parser/mysql_txn/input.sql b/parser/testdata/parser/mysql_txn/input.sql new file mode 100644 index 0000000..4ba4c8f --- /dev/null +++ b/parser/testdata/parser/mysql_txn/input.sql @@ -0,0 +1,37 @@ +XA START 'xid1' +-- case +XA BEGIN 'xid1' +-- case +XA START 'gtrid', 'bqual' +-- case +XA START 'gtrid', 'bqual', 7 +-- case +XA START 'xid1' JOIN +-- case +XA START 'xid1' RESUME +-- case +XA END 'xid1' +-- case +XA END 'gtrid', 'bqual', 7 +-- case +XA END 'xid1' SUSPEND +-- case +XA END 'xid1' SUSPEND FOR MIGRATE +-- case +XA PREPARE 'xid1' +-- case +XA COMMIT 'xid1' +-- case +XA COMMIT 'xid1' ONE PHASE +-- case +XA ROLLBACK 'xid1' +-- case +XA RECOVER +-- case +XA RECOVER CONVERT XID +-- case +LOCK INSTANCE FOR BACKUP +-- case +UNLOCK INSTANCE +-- case +SELECT xa, xid, one, phase FROM suspend WHERE migrate = 1 diff --git a/parser/testdata/parser/mysql_txn/output.sql b/parser/testdata/parser/mysql_txn/output.sql new file mode 100644 index 0000000..8409825 --- /dev/null +++ b/parser/testdata/parser/mysql_txn/output.sql @@ -0,0 +1,37 @@ +XA START 'xid1' +-- case +XA START 'xid1' +-- case +XA START 'gtrid', 'bqual' +-- case +XA START 'gtrid', 'bqual', 7 +-- case +XA START 'xid1' JOIN +-- case +XA START 'xid1' RESUME +-- case +XA END 'xid1' +-- case +XA END 'gtrid', 'bqual', 7 +-- case +XA END 'xid1' SUSPEND +-- case +XA END 'xid1' SUSPEND FOR MIGRATE +-- case +XA PREPARE 'xid1' +-- case +XA COMMIT 'xid1' +-- case +XA COMMIT 'xid1' ONE PHASE +-- case +XA ROLLBACK 'xid1' +-- case +XA RECOVER +-- case +XA RECOVER CONVERT XID +-- case +LOCK INSTANCE FOR BACKUP +-- case +UNLOCK INSTANCE +-- case +SELECT `xa`,`xid`,`one`,`phase` FROM `suspend` WHERE `migrate`=1 diff --git a/parser/testdata/parser/mysql_unsupported_txn/input.sql b/parser/testdata/parser/mysql_unsupported_txn/input.sql deleted file mode 100644 index 5c444ad..0000000 --- a/parser/testdata/parser/mysql_unsupported_txn/input.sql +++ /dev/null @@ -1,17 +0,0 @@ -LOCK INSTANCE FOR BACKUP --- case -UNLOCK INSTANCE --- case -XA START 'xid1' --- case -XA END 'xid1' --- case -XA PREPARE 'xid1' --- case -XA COMMIT 'xid1' --- case -XA COMMIT 'xid1' ONE PHASE --- case -XA ROLLBACK 'xid1' --- case -XA RECOVER diff --git a/parser/testdata/parser/mysql_unsupported_txn/output.sql b/parser/testdata/parser/mysql_unsupported_txn/output.sql deleted file mode 100644 index ab95b66..0000000 --- a/parser/testdata/parser/mysql_unsupported_txn/output.sql +++ /dev/null @@ -1,17 +0,0 @@ --- error: line 1 column 13 near "INSTANCE FOR BACKUP" --- case --- error: line 1 column 15 near "INSTANCE" --- case --- error: line 1 column 2 near "XA START 'xid1'" --- case --- error: line 1 column 2 near "XA END 'xid1'" --- case --- error: line 1 column 2 near "XA PREPARE 'xid1'" --- case --- error: line 1 column 2 near "XA COMMIT 'xid1'" --- case --- error: line 1 column 2 near "XA COMMIT 'xid1' ONE PHASE" --- case --- error: line 1 column 2 near "XA ROLLBACK 'xid1'" --- case --- error: line 1 column 2 near "XA RECOVER" diff --git a/parser/token_kinds.go b/parser/token_kinds.go index a4bad8a..f74f30f 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -543,6 +543,7 @@ const ( metadata = 58051 microsecond = 57788 middleIntType = 57494 + migrate = 58280 min = 58052 minRows = 57791 minValue = 57790 @@ -601,6 +602,7 @@ const ( oltpWriteOnly = 57817 on = 57506 onDuplicate = 57820 + one = 58281 online = 57818 only = 57819 open = 57821 @@ -639,6 +641,7 @@ const ( percentRank = 57517 persist = 58265 pessimistic = 58176 + phase = 58282 pipes = 57359 pipesAsOr = 57838 placement = 58057 @@ -851,6 +854,7 @@ const ( sum = 58090 super = 57942 survivalPreferences = 58091 + suspend = 58283 swaps = 57943 switchGroup = 58092 switchesSym = 57944 @@ -983,6 +987,8 @@ const ( workload = 57992 write = 57592 x509 = 57993 + xa = 58284 + xid = 58285 xor = 57593 yearMonth = 57594 yearType = 57994 From bebfb4ac40b803fd9a449e730bbab9ea67dcb6fa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 21:12:52 +0000 Subject: [PATCH 2/4] Support the MySQL replication statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_replication statement coverage group (MySQL 26.7 §15.4): its error goldens turn into Restore() goldens and the group is renamed to mysql_replication with expanded coverage, following the mysql_admin precedent. Grammar, written from the reference manual since these statements postdate the goyacc grammar (parser/parse_replication.go, which now owns the START, STOP, and PURGE statement heads and routes their non-replication alternatives to parse_txn.go and parse_brie.go; the RESET head stays with parse_mysql_admin.go and routes here): - PURGE BINARY LOGS {TO 'log' | BEFORE datetime_expr} and RESET BINARY LOGS AND GTIDS; the removed pre-8.4 spellings (PURGE MASTER LOGS, RESET MASTER) are not parsed, matching the CHANGE MASTER TO precedent - CHANGE REPLICATION FILTER with all seven filter types — the _DB, _TABLE, _WILD_, and REPLICATE_REWRITE_DB value forms, empty () values, and FOR CHANNEL; the filter names are matched case-insensitively in identifier position like the CHANGE REPLICATION SOURCE TO option names - RESET REPLICA [ALL] [FOR CHANNEL], START REPLICA with thread types, UNTIL options (including bare SQL_AFTER_MTS_GAPS, which makes ReplicationSourceOption.Value optional), the USER/PASSWORD/ DEFAULT_AUTH/PLUGIN_DIR connection options, and FOR CHANNEL; STOP REPLICA with thread types and FOR CHANNEL - START GROUP_REPLICATION [USER=, PASSWORD=, DEFAULT_AUTH=] and STOP GROUP_REPLICATION New AST nodes (ast/replication.go): PurgeBinaryLogsStmt, ResetBinaryLogsAndGtidsStmt, ChangeReplicationFilterStmt (with ReplicationFilter and ReplicationRewriteDB), ResetReplicaStmt, StartReplicaStmt, StopReplicaStmt, StartGroupReplicationStmt, and StopGroupReplicationStmt, plus their SEMCommand strings. StartReplicaStmt and StartGroupReplicationStmt implement SecureText to mask passwords, like ChangeReplicationSourceStmt. Keyword tables: BEFORE becomes a reserved word and FILTER, GROUP_REPLICATION, GTIDS, IO_THREAD, and SQL_THREAD unreserved, matching their MySQL 26.7 classification; TestKeywordsLength counts updated accordingly. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FGwevXQRPY7iPxud2UyaJH --- ast/misc.go | 4 + ast/replication.go | 497 ++++++++++++++++++ ast/sem.go | 56 ++ parser/keyword_classes.go | 5 + parser/keywords.go | 6 + parser/keywords_test.go | 8 +- parser/misc.go | 6 + parser/parse_brie.go | 5 +- parser/parse_mysql_admin.go | 15 +- parser/parse_replication.go | 374 ++++++++++++- parser/parse_txn.go | 3 +- .../parser/mysql_replication/input.sql | 63 +++ .../parser/mysql_replication/output.sql | 63 +++ .../mysql_unsupported_replication/input.sql | 19 - .../mysql_unsupported_replication/output.sql | 19 - parser/token_kinds.go | 6 + 16 files changed, 1089 insertions(+), 60 deletions(-) create mode 100644 ast/replication.go create mode 100644 parser/testdata/parser/mysql_replication/input.sql create mode 100644 parser/testdata/parser/mysql_replication/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_replication/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_replication/output.sql diff --git a/ast/misc.go b/ast/misc.go index 243d5d8..4c08bd1 100644 --- a/ast/misc.go +++ b/ast/misc.go @@ -868,6 +868,10 @@ type ReplicationSourceOption struct { // Restore implements Node interface. func (n *ReplicationSourceOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord(n.Name) + if n.Value == nil { + // A bare option name (START REPLICA UNTIL SQL_AFTER_MTS_GAPS). + return nil + } ctx.WritePlain(" = ") if err := n.Value.Restore(ctx); err != nil { return fmt.Errorf("an error occurred while restore ReplicationSourceOption.Value: %w", err) diff --git a/ast/replication.go b/ast/replication.go new file mode 100644 index 0000000..8bfd5bc --- /dev/null +++ b/ast/replication.go @@ -0,0 +1,497 @@ +// 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 ast + +import ( + "strings" + + "github.com/sqlc-dev/marino/format" +) + +// The MySQL replication statements (MySQL 26.7 §15.4): PURGE BINARY +// LOGS and RESET BINARY LOGS AND GTIDS on the source side, and CHANGE +// REPLICATION FILTER, RESET REPLICA, START/STOP REPLICA, and START/STOP +// GROUP_REPLICATION on the replica side. (CHANGE REPLICATION SOURCE TO +// and its ReplicationSourceOption node live in misc.go.) + +var ( + _ StmtNode = &PurgeBinaryLogsStmt{} + _ StmtNode = &ResetBinaryLogsAndGtidsStmt{} + _ StmtNode = &ChangeReplicationFilterStmt{} + _ StmtNode = &ResetReplicaStmt{} + _ StmtNode = &StartReplicaStmt{} + _ StmtNode = &StopReplicaStmt{} + _ StmtNode = &StartGroupReplicationStmt{} + _ StmtNode = &StopGroupReplicationStmt{} + + _ SensitiveStmtNode = &StartReplicaStmt{} + _ SensitiveStmtNode = &StartGroupReplicationStmt{} +) + +// PurgeBinaryLogsStmt is a PURGE BINARY LOGS statement: +// PURGE BINARY LOGS {TO 'log_name' | BEFORE datetime_expr}. +// Before is nil for the TO form. +type PurgeBinaryLogsStmt struct { + stmtNode + + To string + Before ExprNode +} + +// Restore implements Node interface. +func (n *PurgeBinaryLogsStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("PURGE BINARY LOGS ") + if n.Before != nil { + ctx.WriteKeyWord("BEFORE ") + if err := n.Before.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore PurgeBinaryLogsStmt.Before") + } + return nil + } + ctx.WriteKeyWord("TO ") + ctx.WriteString(n.To) + return nil +} + +// Accept implements Node Accept interface. +func (n *PurgeBinaryLogsStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*PurgeBinaryLogsStmt) + if n.Before != nil { + node, ok := n.Before.Accept(v) + if !ok { + return n, false + } + n.Before = node.(ExprNode) + } + return v.Leave(n) +} + +// ResetBinaryLogsAndGtidsStmt is a RESET BINARY LOGS AND GTIDS +// statement (which replaced RESET MASTER; the removed spelling is not +// parsed). +type ResetBinaryLogsAndGtidsStmt struct { + stmtNode +} + +// Restore implements Node interface. +func (n *ResetBinaryLogsAndGtidsStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("RESET BINARY LOGS AND GTIDS") + return nil +} + +// Accept implements Node Accept interface. +func (n *ResetBinaryLogsAndGtidsStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ResetBinaryLogsAndGtidsStmt) + return v.Leave(n) +} + +// ReplicationFilterType names the filter of one CHANGE REPLICATION +// FILTER specification. +type ReplicationFilterType int + +const ( + // ReplicationFilterDoDB is REPLICATE_DO_DB. + ReplicationFilterDoDB ReplicationFilterType = iota + // ReplicationFilterIgnoreDB is REPLICATE_IGNORE_DB. + ReplicationFilterIgnoreDB + // ReplicationFilterDoTable is REPLICATE_DO_TABLE. + ReplicationFilterDoTable + // ReplicationFilterIgnoreTable is REPLICATE_IGNORE_TABLE. + ReplicationFilterIgnoreTable + // ReplicationFilterWildDoTable is REPLICATE_WILD_DO_TABLE. + ReplicationFilterWildDoTable + // ReplicationFilterWildIgnoreTable is REPLICATE_WILD_IGNORE_TABLE. + ReplicationFilterWildIgnoreTable + // ReplicationFilterRewriteDB is REPLICATE_REWRITE_DB. + ReplicationFilterRewriteDB +) + +// String implements fmt.Stringer interface. +func (n ReplicationFilterType) String() string { + switch n { + case ReplicationFilterDoDB: + return "REPLICATE_DO_DB" + case ReplicationFilterIgnoreDB: + return "REPLICATE_IGNORE_DB" + case ReplicationFilterDoTable: + return "REPLICATE_DO_TABLE" + case ReplicationFilterIgnoreTable: + return "REPLICATE_IGNORE_TABLE" + case ReplicationFilterWildDoTable: + return "REPLICATE_WILD_DO_TABLE" + case ReplicationFilterWildIgnoreTable: + return "REPLICATE_WILD_IGNORE_TABLE" + case ReplicationFilterRewriteDB: + return "REPLICATE_REWRITE_DB" + } + return "" +} + +// ReplicationRewriteDB is one (from_db, to_db) pair of a +// REPLICATE_REWRITE_DB filter. +type ReplicationRewriteDB struct { + FromDB CIStr + ToDB CIStr +} + +// ReplicationFilter is one filter specification of a CHANGE REPLICATION +// FILTER statement. Exactly the value field matching Tp is set — DBNames +// for the _DB filters, Tables for the _TABLE filters, Patterns for the +// _WILD_ filters, and Rewrites for REPLICATE_REWRITE_DB — and an empty +// list (an empty value clears the filter) restores as (). +type ReplicationFilter struct { + Tp ReplicationFilterType + DBNames []CIStr + Tables []*TableName + Patterns []string + Rewrites []*ReplicationRewriteDB +} + +// Restore implements Node interface. +func (n *ReplicationFilter) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord(n.Tp.String()) + ctx.WritePlain(" = (") + switch n.Tp { + case ReplicationFilterDoDB, ReplicationFilterIgnoreDB: + for i, db := range n.DBNames { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WriteName(db.O) + } + case ReplicationFilterDoTable, ReplicationFilterIgnoreTable: + for i, table := range n.Tables { + if i != 0 { + ctx.WritePlain(", ") + } + if err := table.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore ReplicationFilter.Tables[%d]", i) + } + } + case ReplicationFilterWildDoTable, ReplicationFilterWildIgnoreTable: + for i, pattern := range n.Patterns { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WriteString(pattern) + } + case ReplicationFilterRewriteDB: + for i, rewrite := range n.Rewrites { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WritePlain("(") + ctx.WriteName(rewrite.FromDB.O) + ctx.WritePlain(", ") + ctx.WriteName(rewrite.ToDB.O) + ctx.WritePlain(")") + } + } + ctx.WritePlain(")") + return nil +} + +// ChangeReplicationFilterStmt is a CHANGE REPLICATION FILTER statement: +// CHANGE REPLICATION FILTER filter[, filter] ... [FOR CHANNEL channel]. +type ChangeReplicationFilterStmt struct { + stmtNode + + Filters []*ReplicationFilter + Channel string // FOR CHANNEL clause; empty when absent +} + +// Restore implements Node interface. +func (n *ChangeReplicationFilterStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CHANGE REPLICATION FILTER ") + for i, f := range n.Filters { + if i != 0 { + ctx.WritePlain(", ") + } + if err := f.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore ChangeReplicationFilterStmt.Filters[%d]", i) + } + } + if n.Channel != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteString(n.Channel) + } + return nil +} + +// Accept implements Node Accept interface. +func (n *ChangeReplicationFilterStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ChangeReplicationFilterStmt) + for _, f := range n.Filters { + for i, table := range f.Tables { + node, ok := table.Accept(v) + if !ok { + return n, false + } + f.Tables[i] = node.(*TableName) + } + } + return v.Leave(n) +} + +// ResetReplicaStmt is a RESET REPLICA statement: +// RESET REPLICA [ALL] [FOR CHANNEL channel]. +type ResetReplicaStmt struct { + stmtNode + + All bool + Channel string // FOR CHANNEL clause; empty when absent +} + +// Restore implements Node interface. +func (n *ResetReplicaStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("RESET REPLICA") + if n.All { + ctx.WriteKeyWord(" ALL") + } + if n.Channel != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteString(n.Channel) + } + return nil +} + +// Accept implements Node Accept interface. +func (n *ResetReplicaStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ResetReplicaStmt) + return v.Leave(n) +} + +// ReplicaThreadType is one thread type of a START/STOP REPLICA +// statement. +type ReplicaThreadType int + +const ( + // ReplicaThreadIO is IO_THREAD. + ReplicaThreadIO ReplicaThreadType = iota + // ReplicaThreadSQL is SQL_THREAD. + ReplicaThreadSQL +) + +// String implements fmt.Stringer interface. +func (n ReplicaThreadType) String() string { + switch n { + case ReplicaThreadIO: + return "IO_THREAD" + case ReplicaThreadSQL: + return "SQL_THREAD" + } + return "" +} + +func restoreReplicaThreadTypes(ctx *format.RestoreCtx, types []ReplicaThreadType) { + for i, t := range types { + if i != 0 { + ctx.WritePlain(",") + } + ctx.WritePlain(" ") + ctx.WriteKeyWord(t.String()) + } +} + +// StartReplicaStmt is a START REPLICA statement: +// +// START REPLICA [thread_types] [UNTIL until_option] +// [connection_options] [FOR CHANNEL channel] +// +// Until and ConnectionOptions reuse the generic name/value option node +// of CHANGE REPLICATION SOURCE TO; the bare UNTIL SQL_AFTER_MTS_GAPS +// option has a nil Value. +type StartReplicaStmt struct { + stmtNode + + ThreadTypes []ReplicaThreadType + Until []*ReplicationSourceOption + ConnectionOptions []*ReplicationSourceOption + Channel string // FOR CHANNEL clause; empty when absent +} + +// Restore implements Node interface. +func (n *StartReplicaStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("START REPLICA") + restoreReplicaThreadTypes(ctx, n.ThreadTypes) + for i, opt := range n.Until { + if i == 0 { + ctx.WriteKeyWord(" UNTIL ") + } else { + ctx.WritePlain(", ") + } + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore StartReplicaStmt.Until[%d]", i) + } + } + for i, opt := range n.ConnectionOptions { + ctx.WritePlain(" ") + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore StartReplicaStmt.ConnectionOptions[%d]", i) + } + } + if n.Channel != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteString(n.Channel) + } + return nil +} + +// SecureText implements SensitiveStatement interface. +func (n *StartReplicaStmt) SecureText() string { + opts := make([]*ReplicationSourceOption, 0, len(n.ConnectionOptions)) + for _, opt := range n.ConnectionOptions { + if strings.Contains(opt.Name, "PASSWORD") { + opt = &ReplicationSourceOption{Name: opt.Name, Value: NewValueExpr("xxxxxx", "", "")} + } + opts = append(opts, opt) + } + masked := &StartReplicaStmt{ + ThreadTypes: n.ThreadTypes, + Until: n.Until, + ConnectionOptions: 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 *StartReplicaStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*StartReplicaStmt) + return v.Leave(n) +} + +// StopReplicaStmt is a STOP REPLICA statement: +// STOP REPLICA [thread_types] [FOR CHANNEL channel]. +type StopReplicaStmt struct { + stmtNode + + ThreadTypes []ReplicaThreadType + Channel string // FOR CHANNEL clause; empty when absent +} + +// Restore implements Node interface. +func (n *StopReplicaStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("STOP REPLICA") + restoreReplicaThreadTypes(ctx, n.ThreadTypes) + if n.Channel != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteString(n.Channel) + } + return nil +} + +// Accept implements Node Accept interface. +func (n *StopReplicaStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*StopReplicaStmt) + return v.Leave(n) +} + +// StartGroupReplicationStmt is a START GROUP_REPLICATION statement: +// START GROUP_REPLICATION [USER='u' [, PASSWORD='p'] [, DEFAULT_AUTH='a']], +// with the credential options reusing the generic name/value option +// node of CHANGE REPLICATION SOURCE TO. +type StartGroupReplicationStmt struct { + stmtNode + + Options []*ReplicationSourceOption +} + +// Restore implements Node interface. +func (n *StartGroupReplicationStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("START GROUP_REPLICATION") + for i, opt := range n.Options { + if i == 0 { + ctx.WritePlain(" ") + } else { + ctx.WritePlain(", ") + } + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore StartGroupReplicationStmt.Options[%d]", i) + } + } + return nil +} + +// SecureText implements SensitiveStatement interface. +func (n *StartGroupReplicationStmt) 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 := &StartGroupReplicationStmt{Options: opts} + var sb strings.Builder + _ = masked.Restore(format.NewRestoreCtx(format.DefaultRestoreFlags, &sb)) + return sb.String() +} + +// Accept implements Node Accept interface. +func (n *StartGroupReplicationStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*StartGroupReplicationStmt) + return v.Leave(n) +} + +// StopGroupReplicationStmt is a STOP GROUP_REPLICATION statement. +type StopGroupReplicationStmt struct { + stmtNode +} + +// Restore implements Node interface. +func (n *StopGroupReplicationStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("STOP GROUP_REPLICATION") + return nil +} + +// Accept implements Node Accept interface. +func (n *StopGroupReplicationStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*StopGroupReplicationStmt) + return v.Leave(n) +} diff --git a/ast/sem.go b/ast/sem.go index 8230767..b1ea07e 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -542,6 +542,22 @@ const ( LockInstanceCommand = "LOCK INSTANCE" // UnlockInstanceCommand represents UNLOCK INSTANCE statement UnlockInstanceCommand = "UNLOCK INSTANCE" + // PurgeBinaryLogsCommand represents PURGE BINARY LOGS statement + PurgeBinaryLogsCommand = "PURGE BINARY LOGS" + // ResetBinaryLogsAndGtidsCommand represents RESET BINARY LOGS AND GTIDS statement + ResetBinaryLogsAndGtidsCommand = "RESET BINARY LOGS AND GTIDS" + // ChangeReplicationFilterCommand represents CHANGE REPLICATION FILTER statement + ChangeReplicationFilterCommand = "CHANGE REPLICATION FILTER" + // ResetReplicaCommand represents RESET REPLICA statement + ResetReplicaCommand = "RESET REPLICA" + // StartReplicaCommand represents START REPLICA statement + StartReplicaCommand = "START REPLICA" + // StopReplicaCommand represents STOP REPLICA statement + StopReplicaCommand = "STOP REPLICA" + // StartGroupReplicationCommand represents START GROUP_REPLICATION statement + StartGroupReplicationCommand = "START GROUP_REPLICATION" + // StopGroupReplicationCommand represents STOP GROUP_REPLICATION statement + StopGroupReplicationCommand = "STOP GROUP_REPLICATION" // UnknownCommand represents unknown statements UnknownCommand = "UNKNOWN" // SetOprCommand represents UNION/INTERSECT/EXCEPT statement @@ -1547,3 +1563,43 @@ func (n *LockInstanceStmt) SEMCommand() string { func (n *UnlockInstanceStmt) SEMCommand() string { return UnlockInstanceCommand } + +// SEMCommand returns the command string for the statement. +func (n *PurgeBinaryLogsStmt) SEMCommand() string { + return PurgeBinaryLogsCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ResetBinaryLogsAndGtidsStmt) SEMCommand() string { + return ResetBinaryLogsAndGtidsCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ChangeReplicationFilterStmt) SEMCommand() string { + return ChangeReplicationFilterCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ResetReplicaStmt) SEMCommand() string { + return ResetReplicaCommand +} + +// SEMCommand returns the command string for the statement. +func (n *StartReplicaStmt) SEMCommand() string { + return StartReplicaCommand +} + +// SEMCommand returns the command string for the statement. +func (n *StopReplicaStmt) SEMCommand() string { + return StopReplicaCommand +} + +// SEMCommand returns the command string for the statement. +func (n *StartGroupReplicationStmt) SEMCommand() string { + return StartGroupReplicationCommand +} + +// SEMCommand returns the command string for the statement. +func (n *StopGroupReplicationStmt) SEMCommand() string { + return StopGroupReplicationCommand +} diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 2c73b1d..892901e 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -436,6 +436,11 @@ var unReservedKeywordNames = []string{ "SUSPEND", "XA", "XID", + "FILTER", + "GROUP_REPLICATION", + "GTIDS", + "IO_THREAD", + "SQL_THREAD", "CODE", "LIBRARY", "MUTEX", diff --git a/parser/keywords.go b/parser/keywords.go index fb3ca02..143a81b 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -34,6 +34,7 @@ var Keywords = []KeywordsType{ {"ARRAY", true, "reserved"}, {"AS", true, "reserved"}, {"ASC", true, "reserved"}, + {"BEFORE", true, "reserved"}, {"BETWEEN", true, "reserved"}, {"BIGINT", true, "reserved"}, {"BINARY", true, "reserved"}, @@ -402,6 +403,7 @@ var Keywords = []KeywordsType{ {"FAULTS", false, "unreserved"}, {"FIELDS", false, "unreserved"}, {"FILE", false, "unreserved"}, + {"FILTER", false, "unreserved"}, {"FIRST", false, "unreserved"}, {"FIXED", false, "unreserved"}, {"FLUSH", false, "unreserved"}, @@ -413,6 +415,8 @@ var Keywords = []KeywordsType{ {"GENERAL", false, "unreserved"}, {"GLOBAL", false, "unreserved"}, {"GRANTS", false, "unreserved"}, + {"GROUP_REPLICATION", false, "unreserved"}, + {"GTIDS", false, "unreserved"}, {"HANDLER", false, "unreserved"}, {"HASH", false, "unreserved"}, {"HELP", false, "unreserved"}, @@ -435,6 +439,7 @@ var Keywords = []KeywordsType{ {"INVISIBLE", false, "unreserved"}, {"INVOKER", false, "unreserved"}, {"IO", false, "unreserved"}, + {"IO_THREAD", false, "unreserved"}, {"IPC", false, "unreserved"}, {"ISOLATION", false, "unreserved"}, {"ISSUER", false, "unreserved"}, @@ -610,6 +615,7 @@ var Keywords = []KeywordsType{ {"SQL_BUFFER_RESULT", false, "unreserved"}, {"SQL_CACHE", false, "unreserved"}, {"SQL_NO_CACHE", false, "unreserved"}, + {"SQL_THREAD", false, "unreserved"}, {"SQL_TSI_DAY", false, "unreserved"}, {"SQL_TSI_HOUR", false, "unreserved"}, {"SQL_TSI_MINUTE", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 9636bf4..e9c1ea4 100644 --- a/parser/keywords_test.go +++ b/parser/keywords_test.go @@ -43,8 +43,8 @@ func TestKeywords(t *testing.T) { } func TestKeywordsLength(t *testing.T) { - if !reflect.DeepEqual(718, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 718) + if !reflect.DeepEqual(724, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 724) } reservedNr := 0 @@ -53,8 +53,8 @@ func TestKeywordsLength(t *testing.T) { reservedNr += 1 } } - if !reflect.DeepEqual(239, reservedNr) { - t.Fatalf("got %v, want %v", reservedNr, 239) + if !reflect.DeepEqual(240, reservedNr) { + t.Fatalf("got %v, want %v", reservedNr, 240) } } diff --git a/parser/misc.go b/parser/misc.go index 9a71a20..575ea68 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -201,6 +201,7 @@ var tokenMap = map[string]int{ "BACKUP": backup, "BACKUPS": backups, "BDR": bdr, + "BEFORE": before, "BEGIN": begin, "BETWEEN": between, "BERNOULLI": bernoulli, @@ -412,6 +413,7 @@ var tokenMap = map[string]int{ "FETCH": fetch, "FIELDS": fields, "FILE": file, + "FILTER": filter, "FIRST": first, "FIXED": fixed, "FLASHBACK": flashback, @@ -442,7 +444,9 @@ var tokenMap = map[string]int{ "GRANT": grant, "GRANTS": grants, "GROUP_CONCAT": groupConcat, + "GROUP_REPLICATION": groupReplication, "GROUP": group, + "GTIDS": gtids, "HASH": hash, "HANDLER": handler, "HAVING": having, @@ -491,6 +495,7 @@ var tokenMap = map[string]int{ "INVERTED": inverted, "INVISIBLE": invisible, "INVOKER": invoker, + "IO_THREAD": ioThread, "ITERATE": iterate, "IO": io, "RU_PER_SEC": ruRate, @@ -806,6 +811,7 @@ var tokenMap = map[string]int{ "SQL_CALC_FOUND_ROWS": sqlCalcFoundRows, "SQL_NO_CACHE": sqlNoCache, "SQL_SMALL_RESULT": sqlSmallResult, + "SQL_THREAD": sqlThread, "SQL_TSI_DAY": sqlTsiDay, "SQL_TSI_HOUR": sqlTsiHour, "SQL_TSI_MINUTE": sqlTsiMinute, diff --git a/parser/parse_brie.go b/parser/parse_brie.go index 8fb2e0f..819735d 100644 --- a/parser/parse_brie.go +++ b/parser/parse_brie.go @@ -27,12 +27,13 @@ import ( ) func init() { + // The stop and purge tokens are owned by parse_replication.go's + // families, which route "STOP BACKUP LOGS" and "PURGE BACKUP LOGS" + // here. rdRegister(backup, (*rdParser).parseBackupStmt) rdRegister(restore, (*rdParser).parseRestoreStmt) - rdRegister(stop, (*rdParser).parseStopBackupStmt) rdRegister(pause, (*rdParser).parsePauseBackupStmt) rdRegister(resume, (*rdParser).parseResumeBackupStmt) - rdRegister(purge, (*rdParser).parsePurgeBackupStmt) } // parseBackupStmt implements the BRIEStmt alternatives diff --git a/parser/parse_mysql_admin.go b/parser/parse_mysql_admin.go index 4956678..a253990 100644 --- a/parser/parse_mysql_admin.go +++ b/parser/parse_mysql_admin.go @@ -355,11 +355,18 @@ func (r *rdParser) parseCacheIndexName() ast.CIStr { return ast.NewCIStr(r.parseIdentifier()) } -// parseResetStmt implements the RESET statement family, of which only -// ResetPersistStmt is supported: -// "RESET" "PERSIST" [["IF" "EXISTS"] Identifier]. +// parseResetStmt implements the RESET statement family: +// ResetPersistStmt ("RESET" "PERSIST" [["IF" "EXISTS"] Identifier]) +// here, with ResetReplicaStmt and ResetBinaryLogsAndGtidsStmt in +// parse_replication.go. func (r *rdParser) parseResetStmt() ast.StmtNode { - if r.la(1) != persist { + switch r.la(1) { + case replica: + return r.parseResetReplicaStmt() + case binaryType: + return r.parseResetBinaryLogsAndGtidsStmt() + case persist: + default: // The other RESET forms do not parse; fail at RESET the way an // unregistered statement head does. r.syntaxError() diff --git a/parser/parse_replication.go b/parser/parse_replication.go index d13ea97..fcbb465 100644 --- a/parser/parse_replication.go +++ b/parser/parse_replication.go @@ -13,6 +13,22 @@ package parser +// The MySQL replication statements (MySQL 26.7 §15.4): CHANGE +// REPLICATION SOURCE TO, CHANGE REPLICATION FILTER, PURGE BINARY LOGS, +// RESET BINARY LOGS AND GTIDS, RESET REPLICA, START/STOP REPLICA, and +// START/STOP GROUP_REPLICATION. All of these postdate the goyacc +// grammar; the productions are written from the MySQL 26.7 reference +// manual in the same style as parser.y. The removed pre-8.4 spellings +// (CHANGE MASTER TO, PURGE MASTER LOGS, RESET MASTER, RESET/START/STOP +// SLAVE) are not parsed. +// +// This file owns the START, STOP, and PURGE statement heads and routes +// their non-replication alternatives onward: START to +// BeginTransactionStmt (parse_txn.go), STOP and PURGE to the BRIE +// statement family (parse_brie.go). The RESET head stays with +// parse_mysql_admin.go, which routes RESET REPLICA and RESET BINARY +// LOGS AND GTIDS here. + import ( "strings" @@ -20,20 +36,68 @@ import ( ) func init() { - rdRegister(change, (*rdParser).parseChangeReplicationSourceStmt) + rdRegister(change, (*rdParser).parseChangeStmtFamily) + rdRegister(start, (*rdParser).parseStartStmtFamily) + rdRegister(stop, (*rdParser).parseStopStmtFamily) + rdRegister(purge, (*rdParser).parsePurgeStmtFamily) +} + +// parseChangeStmtFamily dispatches the CHANGE statement family: +// ChangeReplicationSourceStmt | ChangeReplicationFilterStmt. +func (r *rdParser) parseChangeStmtFamily() ast.StmtNode { + if r.la(2) == filter { + return r.parseChangeReplicationFilterStmt() + } + return r.parseChangeReplicationSourceStmt() +} + +// parseStartStmtFamily dispatches the START statement family on the +// token after "START": StartReplicaStmt, StartGroupReplicationStmt, or +// the "START TRANSACTION" alternative of BeginTransactionStmt. +func (r *rdParser) parseStartStmtFamily() ast.StmtNode { + switch r.la(1) { + case replica: + return r.parseStartReplicaStmt() + case groupReplication: + return r.parseStartGroupReplicationStmt() + } + return r.parseBeginTransactionStmt() +} + +// parseStopStmtFamily dispatches the STOP statement family on the token +// after "STOP": StopReplicaStmt, StopGroupReplicationStmt, or the +// "STOP BACKUP LOGS" BRIEStmt. +func (r *rdParser) parseStopStmtFamily() ast.StmtNode { + switch r.la(1) { + case replica: + return r.parseStopReplicaStmt() + case groupReplication: + // StopGroupReplicationStmt: "STOP" "GROUP_REPLICATION" + r.advance() + r.advance() + return &ast.StopGroupReplicationStmt{} + } + return r.parseStopBackupStmt() } -// 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: +// parsePurgeStmtFamily dispatches the PURGE statement family on the +// token after "PURGE": PurgeBinaryLogsStmt or the "PURGE BACKUP LOGS" +// BRIEStmt. +func (r *rdParser) parsePurgeStmtFamily() ast.StmtNode { + if r.la(1) == binaryType { + return r.parsePurgeBinaryLogsStmt() + } + return r.parsePurgeBackupStmt() +} + +// parseChangeReplicationSourceStmt implements ChangeReplicationSourceStmt: // // "CHANGE" "REPLICATION" "SOURCE" "TO" ReplicationSourceOption -// ("," ReplicationSourceOption)* ("FOR" "CHANNEL" stringLit)? +// ("," ReplicationSourceOption)* ForChannelOpt // // 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) @@ -45,10 +109,7 @@ func (r *rdParser) parseChangeReplicationSourceStmt() ast.StmtNode { for r.accept(int(',')) { stmt.Options = append(stmt.Options, r.parseReplicationSourceOption()) } - if r.accept(forKwd) { - r.expect(channel) - stmt.Channel = r.expect(stringLit).lit - } + stmt.Channel = r.parseForChannelOpt() return stmt } @@ -57,6 +118,12 @@ func (r *rdParser) parseChangeReplicationSourceStmt() ast.StmtNode { func (r *rdParser) parseReplicationSourceOption() *ast.ReplicationSourceOption { name := strings.ToUpper(r.parseIdentifier()) r.expect(eq) + return &ast.ReplicationSourceOption{Name: name, Value: r.parseReplicationOptionValue()} +} + +// parseReplicationOptionValue implements the literal value of a +// replication option: stringLit | intLit | decLit | floatLit. +func (r *rdParser) parseReplicationOptionValue() ast.ValueExpr { var value ast.ValueExpr switch r.tok() { case stringLit: @@ -68,5 +135,290 @@ func (r *rdParser) parseReplicationSourceOption() *ast.ReplicationSourceOption { default: r.syntaxError() } - return &ast.ReplicationSourceOption{Name: name, Value: value} + return value +} + +// parseForChannelOpt implements ForChannelOpt: +// empty | "FOR" "CHANNEL" stringLit. It returns "" for the empty +// alternative. +func (r *rdParser) parseForChannelOpt() string { + if !r.accept(forKwd) { + return "" + } + r.expect(channel) + return r.expect(stringLit).lit +} + +// parseChangeReplicationFilterStmt implements ChangeReplicationFilterStmt: +// +// "CHANGE" "REPLICATION" "FILTER" ReplicationFilter +// ("," ReplicationFilter)* ForChannelOpt +func (r *rdParser) parseChangeReplicationFilterStmt() ast.StmtNode { + r.expect(change) + r.expect(replication) + r.expect(filter) + stmt := &ast.ChangeReplicationFilterStmt{ + Filters: []*ast.ReplicationFilter{r.parseReplicationFilter()}, + } + for r.accept(int(',')) { + stmt.Filters = append(stmt.Filters, r.parseReplicationFilter()) + } + stmt.Channel = r.parseForChannelOpt() + return stmt +} + +// parseReplicationFilter implements ReplicationFilter. The filter names +// are not keywords (non-reserved in MySQL) and are matched +// case-insensitively in identifier position; the name selects the value +// form: +// +// {"REPLICATE_DO_DB" | "REPLICATE_IGNORE_DB"} eq '(' DBNameListOpt ')' +// | {"REPLICATE_DO_TABLE" | "REPLICATE_IGNORE_TABLE"} eq +// '(' TableNameListOpt ')' +// | {"REPLICATE_WILD_DO_TABLE" | "REPLICATE_WILD_IGNORE_TABLE"} eq +// '(' StringListOpt ')' +// | "REPLICATE_REWRITE_DB" eq '(' DBPairListOpt ')' +// +// An empty '(' ')' value clears the filter. +func (r *rdParser) parseReplicationFilter() *ast.ReplicationFilter { + if !isIdentifierTok(r.tok()) { + r.syntaxError() + } + var tp ast.ReplicationFilterType + switch strings.ToUpper(r.cur().lit) { + case "REPLICATE_DO_DB": + tp = ast.ReplicationFilterDoDB + case "REPLICATE_IGNORE_DB": + tp = ast.ReplicationFilterIgnoreDB + case "REPLICATE_DO_TABLE": + tp = ast.ReplicationFilterDoTable + case "REPLICATE_IGNORE_TABLE": + tp = ast.ReplicationFilterIgnoreTable + case "REPLICATE_WILD_DO_TABLE": + tp = ast.ReplicationFilterWildDoTable + case "REPLICATE_WILD_IGNORE_TABLE": + tp = ast.ReplicationFilterWildIgnoreTable + case "REPLICATE_REWRITE_DB": + tp = ast.ReplicationFilterRewriteDB + default: + r.syntaxError() + } + r.advance() + r.expect(eq) + r.expect(int('(')) + f := &ast.ReplicationFilter{Tp: tp} + if r.tok() != int(')') { + switch tp { + case ast.ReplicationFilterDoDB, ast.ReplicationFilterIgnoreDB: + f.DBNames = []ast.CIStr{ast.NewCIStr(r.parseIdentifier())} + for r.accept(int(',')) { + f.DBNames = append(f.DBNames, ast.NewCIStr(r.parseIdentifier())) + } + case ast.ReplicationFilterDoTable, ast.ReplicationFilterIgnoreTable: + f.Tables = []*ast.TableName{r.parseTableName()} + for r.accept(int(',')) { + f.Tables = append(f.Tables, r.parseTableName()) + } + case ast.ReplicationFilterWildDoTable, ast.ReplicationFilterWildIgnoreTable: + f.Patterns = []string{r.expect(stringLit).lit} + for r.accept(int(',')) { + f.Patterns = append(f.Patterns, r.expect(stringLit).lit) + } + case ast.ReplicationFilterRewriteDB: + f.Rewrites = []*ast.ReplicationRewriteDB{r.parseReplicationRewriteDB()} + for r.accept(int(',')) { + f.Rewrites = append(f.Rewrites, r.parseReplicationRewriteDB()) + } + } + } + r.expect(int(')')) + return f +} + +// parseReplicationRewriteDB implements the (from_db, to_db) pair of a +// REPLICATE_REWRITE_DB filter. +func (r *rdParser) parseReplicationRewriteDB() *ast.ReplicationRewriteDB { + r.expect(int('(')) + pair := &ast.ReplicationRewriteDB{FromDB: ast.NewCIStr(r.parseIdentifier())} + r.expect(int(',')) + pair.ToDB = ast.NewCIStr(r.parseIdentifier()) + r.expect(int(')')) + return pair +} + +// parsePurgeBinaryLogsStmt implements PurgeBinaryLogsStmt: +// "PURGE" "BINARY" "LOGS" ("TO" stringLit | "BEFORE" Expression). +func (r *rdParser) parsePurgeBinaryLogsStmt() ast.StmtNode { + r.expect(purge) + r.expect(binaryType) + r.expect(logs) + switch r.tok() { + case to: + r.advance() + return &ast.PurgeBinaryLogsStmt{To: r.expect(stringLit).lit} + case before: + r.advance() + return &ast.PurgeBinaryLogsStmt{Before: r.parseExpression()} + } + r.syntaxError() + return nil +} + +// parseResetReplicaStmt implements ResetReplicaStmt: +// "RESET" "REPLICA" ["ALL"] ForChannelOpt. +func (r *rdParser) parseResetReplicaStmt() ast.StmtNode { + r.expect(reset) + r.expect(replica) + stmt := &ast.ResetReplicaStmt{All: r.accept(all)} + stmt.Channel = r.parseForChannelOpt() + return stmt +} + +// parseResetBinaryLogsAndGtidsStmt implements +// ResetBinaryLogsAndGtidsStmt: "RESET" "BINARY" "LOGS" "AND" "GTIDS". +func (r *rdParser) parseResetBinaryLogsAndGtidsStmt() ast.StmtNode { + r.expect(reset) + r.expect(binaryType) + r.expect(logs) + r.expect(and) + r.expect(gtids) + return &ast.ResetBinaryLogsAndGtidsStmt{} +} + +// parseReplicaThreadTypes implements the thread_types list of +// START/STOP REPLICA: empty | ReplicaThreadType (',' ReplicaThreadType)* +// with ReplicaThreadType: "IO_THREAD" | "SQL_THREAD". +func (r *rdParser) parseReplicaThreadTypes() []ast.ReplicaThreadType { + if r.tok() != ioThread && r.tok() != sqlThread { + return nil + } + types := []ast.ReplicaThreadType{r.parseReplicaThreadType()} + for r.accept(int(',')) { + types = append(types, r.parseReplicaThreadType()) + } + return types +} + +func (r *rdParser) parseReplicaThreadType() ast.ReplicaThreadType { + switch r.tok() { + case ioThread: + r.advance() + return ast.ReplicaThreadIO + case sqlThread: + r.advance() + return ast.ReplicaThreadSQL + } + r.syntaxError() + return ast.ReplicaThreadIO +} + +// parseStartReplicaStmt implements StartReplicaStmt: +// +// "START" "REPLICA" ReplicaThreadTypes ["UNTIL" ReplicaUntilOption +// ("," ReplicaUntilOption)*] ReplicaConnectionOptions ForChannelOpt +// +// A ReplicaUntilOption is a name eq value pair (SQL_BEFORE_GTIDS, +// SOURCE_LOG_FILE, RELAY_LOG_POS, ...) or the bare SQL_AFTER_MTS_GAPS; +// like the CHANGE REPLICATION SOURCE TO options the names are matched +// in identifier position and not validated against the server's list. +func (r *rdParser) parseStartReplicaStmt() ast.StmtNode { + r.expect(start) + r.expect(replica) + stmt := &ast.StartReplicaStmt{ThreadTypes: r.parseReplicaThreadTypes()} + if r.accept(until) { + stmt.Until = []*ast.ReplicationSourceOption{r.parseReplicaUntilOption()} + for r.accept(int(',')) { + stmt.Until = append(stmt.Until, r.parseReplicaUntilOption()) + } + } + stmt.ConnectionOptions = r.parseReplicaConnectionOptions() + stmt.Channel = r.parseForChannelOpt() + return stmt +} + +// parseReplicaUntilOption implements ReplicaUntilOption: +// Identifier [eq (stringLit | intLit | decLit | floatLit)]. +func (r *rdParser) parseReplicaUntilOption() *ast.ReplicationSourceOption { + name := strings.ToUpper(r.parseIdentifier()) + opt := &ast.ReplicationSourceOption{Name: name} + if r.accept(eq) { + opt.Value = r.parseReplicationOptionValue() + } + return opt +} + +// parseReplicaConnectionOptions implements the space-separated +// connection options of START REPLICA: ("USER" | "PASSWORD" | +// "DEFAULT_AUTH" | "PLUGIN_DIR") eq stringLit, in any order. +// DEFAULT_AUTH and PLUGIN_DIR are matched in identifier position. +func (r *rdParser) parseReplicaConnectionOptions() []*ast.ReplicationSourceOption { + var opts []*ast.ReplicationSourceOption + for { + var name string + switch { + case r.tok() == user || r.tok() == password: + name = strings.ToUpper(r.cur().lit) + case isIdentifierTok(r.tok()): + name = strings.ToUpper(r.cur().lit) + if name != "DEFAULT_AUTH" && name != "PLUGIN_DIR" { + return opts + } + default: + return opts + } + r.advance() + r.expect(eq) + opts = append(opts, &ast.ReplicationSourceOption{ + Name: name, + Value: ast.NewValueExpr(r.expect(stringLit).lit, "", ""), + }) + } +} + +// parseStopReplicaStmt implements StopReplicaStmt: +// "STOP" "REPLICA" ReplicaThreadTypes ForChannelOpt. +func (r *rdParser) parseStopReplicaStmt() ast.StmtNode { + r.expect(stop) + r.expect(replica) + stmt := &ast.StopReplicaStmt{ThreadTypes: r.parseReplicaThreadTypes()} + stmt.Channel = r.parseForChannelOpt() + return stmt +} + +// parseStartGroupReplicationStmt implements StartGroupReplicationStmt: +// +// "START" "GROUP_REPLICATION" [GroupReplicationOption +// ("," GroupReplicationOption)*] +// +// with GroupReplicationOption: ("USER" | "PASSWORD" | "DEFAULT_AUTH") +// eq stringLit; DEFAULT_AUTH is matched in identifier position. +func (r *rdParser) parseStartGroupReplicationStmt() ast.StmtNode { + r.expect(start) + r.expect(groupReplication) + stmt := &ast.StartGroupReplicationStmt{} + if r.tok() != user && r.tok() != password && + !(isIdentifierTok(r.tok()) && strings.EqualFold(r.cur().lit, "DEFAULT_AUTH")) { + return stmt + } + stmt.Options = []*ast.ReplicationSourceOption{r.parseGroupReplicationOption()} + for r.accept(int(',')) { + stmt.Options = append(stmt.Options, r.parseGroupReplicationOption()) + } + return stmt +} + +func (r *rdParser) parseGroupReplicationOption() *ast.ReplicationSourceOption { + switch { + case r.tok() == user || r.tok() == password: + case isIdentifierTok(r.tok()) && strings.EqualFold(r.cur().lit, "DEFAULT_AUTH"): + default: + r.syntaxError() + } + name := strings.ToUpper(r.cur().lit) + r.advance() + r.expect(eq) + return &ast.ReplicationSourceOption{ + Name: name, + Value: ast.NewValueExpr(r.expect(stringLit).lit, "", ""), + } } diff --git a/parser/parse_txn.go b/parser/parse_txn.go index 6a152e3..355eb31 100644 --- a/parser/parse_txn.go +++ b/parser/parse_txn.go @@ -21,8 +21,9 @@ import ( ) func init() { + // The start token is owned by parse_replication.go's + // parseStartStmtFamily, which routes "START TRANSACTION" here. rdRegister(begin, (*rdParser).parseBeginTransactionStmt) - rdRegister(start, (*rdParser).parseBeginTransactionStmt) rdRegister(commit, (*rdParser).parseCommitStmt) rdRegister(rollback, (*rdParser).parseRollbackStmt) rdRegister(savepoint, (*rdParser).parseSavepointStmt) diff --git a/parser/testdata/parser/mysql_replication/input.sql b/parser/testdata/parser/mysql_replication/input.sql new file mode 100644 index 0000000..4a59db3 --- /dev/null +++ b/parser/testdata/parser/mysql_replication/input.sql @@ -0,0 +1,63 @@ +PURGE BINARY LOGS TO 'binlog.000001' +-- case +PURGE BINARY LOGS BEFORE '2026-08-18 00:00:00' +-- case +PURGE BINARY LOGS BEFORE NOW() - INTERVAL 3 DAY +-- case +RESET BINARY LOGS AND GTIDS +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_DB = (db1) +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_DB = (db1, db2), REPLICATE_IGNORE_DB = (db3) +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_TABLE = (db1.t1, db2.t2) +-- case +CHANGE REPLICATION FILTER REPLICATE_IGNORE_TABLE = (db1.t1) +-- case +CHANGE REPLICATION FILTER REPLICATE_WILD_DO_TABLE = ('db1.old%') +-- case +CHANGE REPLICATION FILTER REPLICATE_WILD_IGNORE_TABLE = ('db1.new%', 'db2.%') +-- case +CHANGE REPLICATION FILTER REPLICATE_REWRITE_DB = ((db1, db2)) +-- case +CHANGE REPLICATION FILTER REPLICATE_REWRITE_DB = ((db1, db2), (db3, db4)) FOR CHANNEL 'ch1' +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_DB = () +-- case +RESET REPLICA +-- case +RESET REPLICA ALL +-- case +RESET REPLICA ALL FOR CHANNEL 'ch1' +-- case +START REPLICA +-- case +START REPLICA IO_THREAD +-- case +START REPLICA IO_THREAD, SQL_THREAD +-- case +START REPLICA UNTIL SQL_BEFORE_GTIDS = '3E11FA47-71CA-11E1-9E33-C80AA9429562:11-56' +-- case +START REPLICA UNTIL SQL_AFTER_GTIDS = '3E11FA47-71CA-11E1-9E33-C80AA9429562:11-56' +-- case +START REPLICA UNTIL SOURCE_LOG_FILE = 'source1-bin.000291', SOURCE_LOG_POS = 137 +-- case +START REPLICA SQL_THREAD UNTIL RELAY_LOG_FILE = 'replica-relay-bin.000015', RELAY_LOG_POS = 5722 +-- case +START REPLICA UNTIL SQL_AFTER_MTS_GAPS +-- case +START REPLICA USER = 'u' PASSWORD = 'p' DEFAULT_AUTH = 'auth_plugin' FOR CHANNEL 'ch1' +-- case +STOP REPLICA +-- case +STOP REPLICA SQL_THREAD FOR CHANNEL 'ch1' +-- case +STOP REPLICA IO_THREAD, SQL_THREAD +-- case +START GROUP_REPLICATION +-- case +START GROUP_REPLICATION USER = 'u', PASSWORD = 'p', DEFAULT_AUTH = 'auth_plugin' +-- case +STOP GROUP_REPLICATION +-- case +SELECT filter, gtids, io_thread, sql_thread FROM group_replication diff --git a/parser/testdata/parser/mysql_replication/output.sql b/parser/testdata/parser/mysql_replication/output.sql new file mode 100644 index 0000000..1b70d41 --- /dev/null +++ b/parser/testdata/parser/mysql_replication/output.sql @@ -0,0 +1,63 @@ +PURGE BINARY LOGS TO 'binlog.000001' +-- case +PURGE BINARY LOGS BEFORE _UTF8MB4'2026-08-18 00:00:00' +-- case +PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 3 DAY) +-- case +RESET BINARY LOGS AND GTIDS +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_DB = (`db1`) +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_DB = (`db1`, `db2`), REPLICATE_IGNORE_DB = (`db3`) +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_TABLE = (`db1`.`t1`, `db2`.`t2`) +-- case +CHANGE REPLICATION FILTER REPLICATE_IGNORE_TABLE = (`db1`.`t1`) +-- case +CHANGE REPLICATION FILTER REPLICATE_WILD_DO_TABLE = ('db1.old%') +-- case +CHANGE REPLICATION FILTER REPLICATE_WILD_IGNORE_TABLE = ('db1.new%', 'db2.%') +-- case +CHANGE REPLICATION FILTER REPLICATE_REWRITE_DB = ((`db1`, `db2`)) +-- case +CHANGE REPLICATION FILTER REPLICATE_REWRITE_DB = ((`db1`, `db2`), (`db3`, `db4`)) FOR CHANNEL 'ch1' +-- case +CHANGE REPLICATION FILTER REPLICATE_DO_DB = () +-- case +RESET REPLICA +-- case +RESET REPLICA ALL +-- case +RESET REPLICA ALL FOR CHANNEL 'ch1' +-- case +START REPLICA +-- case +START REPLICA IO_THREAD +-- case +START REPLICA IO_THREAD, SQL_THREAD +-- case +START REPLICA UNTIL SQL_BEFORE_GTIDS = '3E11FA47-71CA-11E1-9E33-C80AA9429562:11-56' +-- case +START REPLICA UNTIL SQL_AFTER_GTIDS = '3E11FA47-71CA-11E1-9E33-C80AA9429562:11-56' +-- case +START REPLICA UNTIL SOURCE_LOG_FILE = 'source1-bin.000291', SOURCE_LOG_POS = 137 +-- case +START REPLICA SQL_THREAD UNTIL RELAY_LOG_FILE = 'replica-relay-bin.000015', RELAY_LOG_POS = 5722 +-- case +START REPLICA UNTIL SQL_AFTER_MTS_GAPS +-- case +START REPLICA USER = 'u' PASSWORD = 'p' DEFAULT_AUTH = 'auth_plugin' FOR CHANNEL 'ch1' +-- case +STOP REPLICA +-- case +STOP REPLICA SQL_THREAD FOR CHANNEL 'ch1' +-- case +STOP REPLICA IO_THREAD, SQL_THREAD +-- case +START GROUP_REPLICATION +-- case +START GROUP_REPLICATION USER = 'u', PASSWORD = 'p', DEFAULT_AUTH = 'auth_plugin' +-- case +STOP GROUP_REPLICATION +-- case +SELECT `filter`,`gtids`,`io_thread`,`sql_thread` FROM `group_replication` diff --git a/parser/testdata/parser/mysql_unsupported_replication/input.sql b/parser/testdata/parser/mysql_unsupported_replication/input.sql deleted file mode 100644 index 43a863c..0000000 --- a/parser/testdata/parser/mysql_unsupported_replication/input.sql +++ /dev/null @@ -1,19 +0,0 @@ -PURGE BINARY LOGS TO 'binlog.000001' --- case -PURGE BINARY LOGS BEFORE '2026-08-18 00:00:00' --- case -RESET BINARY LOGS AND GTIDS --- case -CHANGE REPLICATION FILTER REPLICATE_DO_DB = (db1) --- case -RESET REPLICA --- case -RESET REPLICA ALL --- case -START REPLICA --- case -STOP REPLICA --- case -START GROUP_REPLICATION --- case -STOP GROUP_REPLICATION diff --git a/parser/testdata/parser/mysql_unsupported_replication/output.sql b/parser/testdata/parser/mysql_unsupported_replication/output.sql deleted file mode 100644 index a3c8d9a..0000000 --- a/parser/testdata/parser/mysql_unsupported_replication/output.sql +++ /dev/null @@ -1,19 +0,0 @@ --- error: line 1 column 12 near "BINARY LOGS TO 'binlog.000001'" --- case --- error: line 1 column 12 near "BINARY LOGS BEFORE '2026-08-18 00:00:00'" --- case --- error: line 1 column 5 near "RESET BINARY LOGS AND GTIDS" --- case --- error: line 1 column 25 near "FILTER REPLICATE_DO_DB = (db1)" --- case --- error: line 1 column 5 near "RESET REPLICA" --- case --- error: line 1 column 5 near "RESET REPLICA ALL" --- case --- error: line 1 column 13 near "REPLICA" --- case --- error: line 1 column 12 near "REPLICA" --- case --- error: line 1 column 23 near "GROUP_REPLICATION" --- case --- error: line 1 column 22 near "GROUP_REPLICATION" diff --git a/parser/token_kinds.go b/parser/token_kinds.go index f74f30f..daaf253 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -98,6 +98,7 @@ const ( backups = 57620 batch = 58126 bdr = 57621 + before = 58286 begin = 57622 bernoulli = 57623 between = 57371 @@ -338,6 +339,7 @@ const ( fetch = 57426 fields = 57723 file = 57724 + filter = 58287 first = 57725 firstValue = 57427 fixed = 57726 @@ -373,6 +375,8 @@ const ( group = 57438 groupConcat = 58031 groups = 57439 + groupReplication = 58288 + gtids = 58289 handler = 57736 hash = 57737 having = 57440 @@ -434,6 +438,7 @@ const ( inverted = 58036 invisible = 57754 invoker = 57755 + ioThread = 58290 io = 57756 ioReadBandwidth = 58037 ioWriteBandwidth = 58038 @@ -797,6 +802,7 @@ const ( sqlCalcFoundRows = 57551 sqlNoCache = 57919 sqlSmallResult = 57552 + sqlThread = 58291 sqlTsiDay = 57920 sqlTsiHour = 57921 sqlTsiMinute = 57922 From 46006491afb76657af047abdcbb7a0bf8f48b59b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 21:23:35 +0000 Subject: [PATCH 3/4] Support the MySQL HANDLER, IMPORT TABLE, LOAD XML, and SELECT INTO var statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_dml statement coverage group (MySQL 26.7 §15.2): its error goldens turn into Restore() goldens and the group is renamed to mysql_dml with expanded coverage, following the mysql_admin precedent. Grammar, written from the reference manual since these statements postdate the goyacc grammar: - HANDLER ... OPEN [[AS] alias], HANDLER ... READ with the indexed compare form (= <= >= < > with a value list), the indexed and table-scan direction forms (FIRST/NEXT/PREV/LAST), WHERE, and LIMIT, and HANDLER ... CLOSE (parser/parse_handler.go) - IMPORT TABLE FROM sdi_file [, sdi_file] ... and LOAD XML with LOW_PRIORITY/CONCURRENT, LOCAL, REPLACE/IGNORE, CHARACTER SET, ROWS IDENTIFIED BY, IGNORE n {LINES|ROWS} (canonicalized to ROWS), the column/user-var list, and SET assignments, reusing the LOAD DATA helpers (parser/parse_dml.go) - The SELECT ... INTO var_list and INTO DUMPFILE forms of SelectStmtIntoOption, filling in the SelectIntoVars and SelectIntoDumpfile enum values that existed unimplemented. The INTO clause also parses between the field list and FROM, restoring in the trailing position. Variable targets are user variables or stored program variable names; they carry no origin position because SelectStmt.Accept does not traverse SelectIntoOpt. New AST nodes (ast/mysql_dml.go): HandlerOpenStmt, HandlerReadStmt, HandlerCloseStmt, ImportTableStmt, and LoadXMLStmt, plus their SEMCommand strings. SelectIntoOption gains a Vars field and its Restore supports all three forms. Keyword tables: CONCURRENT, DUMPFILE, PREV, and XML become unreserved keywords, matching their MySQL 26.7 classification; TestKeywordsLength counts updated accordingly. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FGwevXQRPY7iPxud2UyaJH --- ast/dml.go | 32 +- ast/mysql_dml.go | 417 ++++++++++++++++++ ast/sem.go | 35 ++ parser/keyword_classes.go | 4 + parser/keywords.go | 4 + parser/keywords_test.go | 4 +- parser/misc.go | 4 + parser/parse_dml.go | 84 ++++ parser/parse_handler.go | 120 +++++ parser/parse_select.go | 71 ++- parser/rd_parser.go | 12 +- parser/testdata/parser/mysql_dml/input.sql | 51 +++ parser/testdata/parser/mysql_dml/output.sql | 51 +++ .../parser/mysql_unsupported_dml/input.sql | 13 - .../parser/mysql_unsupported_dml/output.sql | 13 - parser/token_kinds.go | 4 + 16 files changed, 875 insertions(+), 44 deletions(-) create mode 100644 ast/mysql_dml.go create mode 100644 parser/parse_handler.go create mode 100644 parser/testdata/parser/mysql_dml/input.sql create mode 100644 parser/testdata/parser/mysql_dml/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_dml/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_dml/output.sql diff --git a/ast/dml.go b/ast/dml.go index d46a3ca..1daf481 100644 --- a/ast/dml.go +++ b/ast/dml.go @@ -3896,12 +3896,32 @@ type SelectIntoOption struct { FileName string FieldsInfo *FieldsClause LinesInfo *LinesClause + // Vars is the variable list of the SelectIntoVars form: user + // variables and, in stored programs, program variables (restored as + // plain names). + Vars []ExprNode } // Restore implements Node interface. func (n *SelectIntoOption) Restore(ctx *format.RestoreCtx) error { - if n.Tp != SelectIntoOutfile { - // only support SELECT/TABLE/VALUES ... INTO OUTFILE statement now + switch n.Tp { + case SelectIntoOutfile: + case SelectIntoDumpfile: + ctx.WriteKeyWord("INTO DUMPFILE ") + ctx.WriteString(n.FileName) + return nil + case SelectIntoVars: + ctx.WriteKeyWord("INTO ") + for i, v := range n.Vars { + if i != 0 { + ctx.WritePlain(", ") + } + if err := v.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore SelectInto.Vars[%d]", i) + } + } + return nil + default: return errors.New("Unsupported SelectionInto type") } @@ -3926,6 +3946,14 @@ func (n *SelectIntoOption) Accept(v Visitor) (Node, bool) { if skipChildren { return v.Leave(newNode) } + n = newNode.(*SelectIntoOption) + for i, v2 := range n.Vars { + node, ok := v2.Accept(v) + if !ok { + return n, false + } + n.Vars[i] = node.(ExprNode) + } return v.Leave(n) } diff --git a/ast/mysql_dml.go b/ast/mysql_dml.go new file mode 100644 index 0000000..86c887e --- /dev/null +++ b/ast/mysql_dml.go @@ -0,0 +1,417 @@ +// 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 ast + +import ( + "github.com/sqlc-dev/marino/format" +) + +// The MySQL data manipulation statements that postdate the goyacc +// grammar (MySQL 26.7 §15.2): HANDLER, IMPORT TABLE, and LOAD XML. + +var ( + _ StmtNode = &HandlerOpenStmt{} + _ StmtNode = &HandlerReadStmt{} + _ StmtNode = &HandlerCloseStmt{} + _ StmtNode = &ImportTableStmt{} + _ StmtNode = &LoadXMLStmt{} +) + +// HandlerOpenStmt is a HANDLER ... OPEN statement: +// HANDLER tbl_name OPEN [[AS] alias]. +type HandlerOpenStmt struct { + stmtNode + + Table *TableName + Alias CIStr // empty when absent; restored with AS +} + +// Restore implements Node interface. +func (n *HandlerOpenStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("HANDLER ") + if err := n.Table.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore HandlerOpenStmt.Table") + } + ctx.WriteKeyWord(" OPEN") + if n.Alias.O != "" { + ctx.WriteKeyWord(" AS ") + ctx.WriteName(n.Alias.O) + } + return nil +} + +// Accept implements Node Accept interface. +func (n *HandlerOpenStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*HandlerOpenStmt) + node, ok := n.Table.Accept(v) + if !ok { + return n, false + } + n.Table = node.(*TableName) + return v.Leave(n) +} + +// HandlerCompareOp is the comparison operator of the indexed-read form +// of HANDLER ... READ. HandlerOpNone selects the direction form. +type HandlerCompareOp int + +const ( + // HandlerOpNone selects the FIRST/NEXT/PREV/LAST direction form. + HandlerOpNone HandlerCompareOp = iota + // HandlerOpEQ is =. + HandlerOpEQ + // HandlerOpLE is <=. + HandlerOpLE + // HandlerOpGE is >=. + HandlerOpGE + // HandlerOpLT is <. + HandlerOpLT + // HandlerOpGT is >. + HandlerOpGT +) + +// String implements fmt.Stringer interface. +func (n HandlerCompareOp) String() string { + switch n { + case HandlerOpEQ: + return "=" + case HandlerOpLE: + return "<=" + case HandlerOpGE: + return ">=" + case HandlerOpLT: + return "<" + case HandlerOpGT: + return ">" + } + return "" +} + +// HandlerReadDirection is the cursor direction of HANDLER ... READ. +type HandlerReadDirection int + +const ( + // HandlerReadFirst is FIRST. + HandlerReadFirst HandlerReadDirection = iota + // HandlerReadNext is NEXT. + HandlerReadNext + // HandlerReadPrev is PREV. + HandlerReadPrev + // HandlerReadLast is LAST. + HandlerReadLast +) + +// String implements fmt.Stringer interface. +func (n HandlerReadDirection) String() string { + switch n { + case HandlerReadFirst: + return "FIRST" + case HandlerReadNext: + return "NEXT" + case HandlerReadPrev: + return "PREV" + case HandlerReadLast: + return "LAST" + } + return "" +} + +// HandlerReadStmt is a HANDLER ... READ statement: +// +// HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value, ...) +// [WHERE where_condition] [LIMIT ...] +// HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST } +// [WHERE where_condition] [LIMIT ...] +// HANDLER tbl_name READ { FIRST | NEXT } +// [WHERE where_condition] [LIMIT ...] +// +// IndexName is empty for the table-scan form. Op is HandlerOpNone for +// the direction forms; otherwise Values holds the comparison values and +// Direction is unused. +type HandlerReadStmt struct { + stmtNode + + Table *TableName + IndexName CIStr + Op HandlerCompareOp + Values []ExprNode + Direction HandlerReadDirection + Where ExprNode + Limit *Limit +} + +// Restore implements Node interface. +func (n *HandlerReadStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("HANDLER ") + if err := n.Table.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore HandlerReadStmt.Table") + } + ctx.WriteKeyWord(" READ ") + if n.IndexName.O != "" { + ctx.WriteName(n.IndexName.O) + ctx.WritePlain(" ") + } + if n.Op != HandlerOpNone { + ctx.WritePlain(n.Op.String()) + ctx.WritePlain(" (") + for i, value := range n.Values { + if i != 0 { + ctx.WritePlain(", ") + } + if err := value.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore HandlerReadStmt.Values[%d]", i) + } + } + ctx.WritePlain(")") + } else { + ctx.WriteKeyWord(n.Direction.String()) + } + if n.Where != nil { + ctx.WriteKeyWord(" WHERE ") + if err := n.Where.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore HandlerReadStmt.Where") + } + } + if n.Limit != nil { + ctx.WritePlain(" ") + if err := n.Limit.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore HandlerReadStmt.Limit") + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *HandlerReadStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*HandlerReadStmt) + node, ok := n.Table.Accept(v) + if !ok { + return n, false + } + n.Table = node.(*TableName) + for i, value := range n.Values { + node, ok := value.Accept(v) + if !ok { + return n, false + } + n.Values[i] = node.(ExprNode) + } + if n.Where != nil { + node, ok := n.Where.Accept(v) + if !ok { + return n, false + } + n.Where = node.(ExprNode) + } + if n.Limit != nil { + node, ok := n.Limit.Accept(v) + if !ok { + return n, false + } + n.Limit = node.(*Limit) + } + return v.Leave(n) +} + +// HandlerCloseStmt is a HANDLER ... CLOSE statement: +// HANDLER tbl_name CLOSE. +type HandlerCloseStmt struct { + stmtNode + + Table *TableName +} + +// Restore implements Node interface. +func (n *HandlerCloseStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("HANDLER ") + if err := n.Table.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore HandlerCloseStmt.Table") + } + ctx.WriteKeyWord(" CLOSE") + return nil +} + +// Accept implements Node Accept interface. +func (n *HandlerCloseStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*HandlerCloseStmt) + node, ok := n.Table.Accept(v) + if !ok { + return n, false + } + n.Table = node.(*TableName) + return v.Leave(n) +} + +// ImportTableStmt is an IMPORT TABLE statement: +// IMPORT TABLE FROM sdi_file [, sdi_file] ... +// (ImportIntoStmt is the unrelated TiDB IMPORT INTO statement.) +type ImportTableStmt struct { + stmtNode + + SdiFiles []string +} + +// Restore implements Node interface. +func (n *ImportTableStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("IMPORT TABLE FROM ") + for i, file := range n.SdiFiles { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WriteString(file) + } + return nil +} + +// Accept implements Node Accept interface. +func (n *ImportTableStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ImportTableStmt) + return v.Leave(n) +} + +// LoadXMLStmt is a LOAD XML statement: +// +// LOAD XML [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name' +// [REPLACE | IGNORE] INTO TABLE [db_name.]tbl_name +// [CHARACTER SET charset_name] +// [ROWS IDENTIFIED BY ''] [IGNORE number {LINES | ROWS}] +// [(field_name_or_user_var [, field_name_or_user_var] ...)] +// [SET (col_name={expr | DEFAULT}) [, col_name={expr | DEFAULT}] ...] +// +// The IGNORE number LINES spelling parses like ROWS, so Restore() +// canonicalizes it to ROWS. +type LoadXMLStmt struct { + stmtNode + + LowPriority bool + Concurrent bool + FileLocRef FileLocRefTp + Path string + OnDuplicate OnDuplicateKeyHandlingType + Table *TableName + Charset *string + RowsIdentifiedBy string // the '' literal; empty when absent + IgnoreRows *uint64 + ColumnsAndUserVars []*ColumnNameOrUserVar + ColumnAssignments []*Assignment +} + +// Restore implements Node interface. +func (n *LoadXMLStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("LOAD XML ") + if n.LowPriority { + ctx.WriteKeyWord("LOW_PRIORITY ") + } else if n.Concurrent { + ctx.WriteKeyWord("CONCURRENT ") + } + if n.FileLocRef == FileLocClient { + ctx.WriteKeyWord("LOCAL ") + } + ctx.WriteKeyWord("INFILE ") + ctx.WriteString(n.Path) + if n.OnDuplicate == OnDuplicateKeyHandlingReplace { + ctx.WriteKeyWord(" REPLACE") + } else if n.OnDuplicate == OnDuplicateKeyHandlingIgnore { + ctx.WriteKeyWord(" IGNORE") + } + ctx.WriteKeyWord(" INTO TABLE ") + if err := n.Table.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore LoadXMLStmt.Table") + } + if n.Charset != nil { + ctx.WriteKeyWord(" CHARACTER SET ") + ctx.WritePlain(*n.Charset) + } + if n.RowsIdentifiedBy != "" { + ctx.WriteKeyWord(" ROWS IDENTIFIED BY ") + ctx.WriteString(n.RowsIdentifiedBy) + } + if n.IgnoreRows != nil { + ctx.WriteKeyWord(" IGNORE ") + ctx.WritePlainf("%d", *n.IgnoreRows) + ctx.WriteKeyWord(" ROWS") + } + if len(n.ColumnsAndUserVars) != 0 { + ctx.WritePlain(" (") + for i, c := range n.ColumnsAndUserVars { + if i != 0 { + ctx.WritePlain(",") + } + if err := c.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore LoadXMLStmt.ColumnsAndUserVars[%d]", i) + } + } + ctx.WritePlain(")") + } + if len(n.ColumnAssignments) != 0 { + ctx.WriteKeyWord(" SET") + for i, assign := range n.ColumnAssignments { + if i != 0 { + ctx.WritePlain(",") + } + ctx.WritePlain(" ") + if err := assign.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore LoadXMLStmt.ColumnAssignments[%d]", i) + } + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *LoadXMLStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*LoadXMLStmt) + if n.Table != nil { + node, ok := n.Table.Accept(v) + if !ok { + return n, false + } + n.Table = node.(*TableName) + } + for i, cuVars := range n.ColumnsAndUserVars { + node, ok := cuVars.Accept(v) + if !ok { + return n, false + } + n.ColumnsAndUserVars[i] = node.(*ColumnNameOrUserVar) + } + for i, assignment := range n.ColumnAssignments { + node, ok := assignment.Accept(v) + if !ok { + return n, false + } + n.ColumnAssignments[i] = node.(*Assignment) + } + return v.Leave(n) +} diff --git a/ast/sem.go b/ast/sem.go index b1ea07e..abb87ba 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -558,6 +558,16 @@ const ( StartGroupReplicationCommand = "START GROUP_REPLICATION" // StopGroupReplicationCommand represents STOP GROUP_REPLICATION statement StopGroupReplicationCommand = "STOP GROUP_REPLICATION" + // HandlerOpenCommand represents HANDLER ... OPEN statement + HandlerOpenCommand = "HANDLER OPEN" + // HandlerReadCommand represents HANDLER ... READ statement + HandlerReadCommand = "HANDLER READ" + // HandlerCloseCommand represents HANDLER ... CLOSE statement + HandlerCloseCommand = "HANDLER CLOSE" + // ImportTableCommand represents IMPORT TABLE statement + ImportTableCommand = "IMPORT TABLE" + // LoadXMLCommand represents LOAD XML statement + LoadXMLCommand = "LOAD XML" // UnknownCommand represents unknown statements UnknownCommand = "UNKNOWN" // SetOprCommand represents UNION/INTERSECT/EXCEPT statement @@ -1603,3 +1613,28 @@ func (n *StartGroupReplicationStmt) SEMCommand() string { func (n *StopGroupReplicationStmt) SEMCommand() string { return StopGroupReplicationCommand } + +// SEMCommand returns the command string for the statement. +func (n *HandlerOpenStmt) SEMCommand() string { + return HandlerOpenCommand +} + +// SEMCommand returns the command string for the statement. +func (n *HandlerReadStmt) SEMCommand() string { + return HandlerReadCommand +} + +// SEMCommand returns the command string for the statement. +func (n *HandlerCloseStmt) SEMCommand() string { + return HandlerCloseCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ImportTableStmt) SEMCommand() string { + return ImportTableCommand +} + +// SEMCommand returns the command string for the statement. +func (n *LoadXMLStmt) SEMCommand() string { + return LoadXMLCommand +} diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 892901e..e8b5d7a 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -441,6 +441,10 @@ var unReservedKeywordNames = []string{ "GTIDS", "IO_THREAD", "SQL_THREAD", + "CONCURRENT", + "DUMPFILE", + "PREV", + "XML", "CODE", "LIBRARY", "MUTEX", diff --git a/parser/keywords.go b/parser/keywords.go index 143a81b..4605fb2 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -341,6 +341,7 @@ var Keywords = []KeywordsType{ {"COMPRESSION_LEVEL", false, "unreserved"}, {"COMPRESSION_TYPE", false, "unreserved"}, {"CONCURRENCY", false, "unreserved"}, + {"CONCURRENT", false, "unreserved"}, {"CONFIG", false, "unreserved"}, {"CONNECTION", false, "unreserved"}, {"CONSISTENCY", false, "unreserved"}, @@ -372,6 +373,7 @@ var Keywords = []KeywordsType{ {"DISCARD", false, "unreserved"}, {"DISK", false, "unreserved"}, {"DO", false, "unreserved"}, + {"DUMPFILE", false, "unreserved"}, {"DUPLICATE", false, "unreserved"}, {"DYNAMIC", false, "unreserved"}, {"ENABLE", false, "unreserved"}, @@ -537,6 +539,7 @@ var Keywords = []KeywordsType{ {"PRECEDING", false, "unreserved"}, {"PREPARE", false, "unreserved"}, {"PRESERVE", false, "unreserved"}, + {"PREV", false, "unreserved"}, {"PRE_SPLIT_REGIONS", false, "unreserved"}, {"PRIVILEGES", false, "unreserved"}, {"PROCESS", false, "unreserved"}, @@ -700,6 +703,7 @@ var Keywords = []KeywordsType{ {"X509", false, "unreserved"}, {"XA", false, "unreserved"}, {"XID", false, "unreserved"}, + {"XML", false, "unreserved"}, {"YEAR", false, "unreserved"}, {"ADMIN", false, "tidb"}, {"BATCH", false, "tidb"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index e9c1ea4..1a60048 100644 --- a/parser/keywords_test.go +++ b/parser/keywords_test.go @@ -43,8 +43,8 @@ func TestKeywords(t *testing.T) { } func TestKeywordsLength(t *testing.T) { - if !reflect.DeepEqual(724, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 724) + if !reflect.DeepEqual(728, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 728) } reservedNr := 0 diff --git a/parser/misc.go b/parser/misc.go index 575ea68..1d7be26 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -276,6 +276,7 @@ var tokenMap = map[string]int{ "COMPRESS": compress, "COMPRESSED": compressed, "COMPRESSION": compression, + "CONCURRENT": concurrent, "CONCURRENCY": concurrency, "CONFIG": config, "CONNECTION": connection, @@ -367,6 +368,7 @@ var tokenMap = map[string]int{ "DRYRUN": dryRun, "DUAL": dual, "DUMP": dump, + "DUMPFILE": dumpfile, "DUPLICATE": duplicate, "DURATION": timeDuration, "DYNAMIC": dynamic, @@ -678,6 +680,7 @@ var tokenMap = map[string]int{ "PRECISION": precisionType, "PREPARE": prepare, "PRESERVE": preserve, + "PREV": prev, "PRIMARY": primary, "PRIMARY_REGION": primaryRegion, "PRIVILEGES": privileges, @@ -993,6 +996,7 @@ var tokenMap = map[string]int{ "X509": x509, "XA": xa, "XID": xid, + "XML": xml, "XOR": xor, "YEAR_MONTH": yearMonth, "YEAR": yearType, diff --git a/parser/parse_dml.go b/parser/parse_dml.go index db78d03..ea515df 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -434,6 +434,90 @@ func (r *rdParser) parseLoadDataStmt() ast.StmtNode { return x } +// parseImportTableStmt implements ImportTableStmt (MySQL 26.7 §15.2.6): +// "IMPORT" "TABLE" "FROM" stringLit (',' stringLit)*. +// The statement postdates the goyacc grammar; the shape follows the +// MySQL 26.7 reference manual. +func (r *rdParser) parseImportTableStmt() ast.StmtNode { + r.expect(importKwd) + r.expect(tableKwd) + r.expect(from) + stmt := &ast.ImportTableStmt{SdiFiles: []string{r.expect(stringLit).lit}} + for r.accept(int(',')) { + stmt.SdiFiles = append(stmt.SdiFiles, r.expect(stringLit).lit) + } + return stmt +} + +// parseLoadXMLStmt implements LoadXMLStmt (MySQL 26.7 §15.2.10): +// +// "LOAD" "XML" ["LOW_PRIORITY" | "CONCURRENT"] ["LOCAL"] "INFILE" +// stringLit ["REPLACE" | "IGNORE"] "INTO" "TABLE" TableName +// [CharsetKw CharsetName] ["ROWS" "IDENTIFIED" "BY" stringLit] +// ["IGNORE" NUM ("LINES" | "ROWS")] +// ColumnNameOrUserVarListOptWithBrackets ["SET" LoadDataSetList] +// +// The statement postdates the goyacc grammar; the shape follows the +// MySQL 26.7 reference manual, reusing the LOAD DATA helpers. +func (r *rdParser) parseLoadXMLStmt() ast.StmtNode { + r.expect(load) + r.expect(xml) + x := &ast.LoadXMLStmt{FileLocRef: ast.FileLocServerOrRemote} + switch r.tok() { + case lowPriority: + r.advance() + x.LowPriority = true + case concurrent: + r.advance() + x.Concurrent = true + } + if r.accept(local) { + x.FileLocRef = ast.FileLocClient + } + r.expect(infile) + x.Path = r.expect(stringLit).lit + x.OnDuplicate = ast.OnDuplicateKeyHandlingError + switch r.tok() { + case ignore: + r.advance() + x.OnDuplicate = ast.OnDuplicateKeyHandlingIgnore + case replace: + r.advance() + x.OnDuplicate = ast.OnDuplicateKeyHandlingReplace + } + r.expect(into) + r.expect(tableKwd) + x.Table = r.parseTableName() + if (r.tok() == character || r.tok() == charType) && r.la(1) == set { + r.advance() + r.advance() + cs := r.parseCharsetName() + x.Charset = &cs + } + if r.tok() == rows && r.la(1) == identified { + r.advance() + r.advance() + r.expect(by) + x.RowsIdentifiedBy = r.expect(stringLit).lit + } + if r.tok() == ignore { + // "IGNORE" NUM ("LINES" | "ROWS"); Restore() canonicalizes the + // LINES spelling to ROWS. + r.advance() + v := getUint64FromNUM(r.expect(intLit).item) + if r.tok() != lines && r.tok() != rows { + r.syntaxError() + } + r.advance() + x.IgnoreRows = &v + } + x.ColumnsAndUserVars = r.parseColumnNameOrUserVarListOptWithBrackets() + if r.accept(set) { + x.ColumnAssignments = r.parseLoadDataSetList() + } + return x +} + // parseColumnNameOrUserVarListOptWithBrackets implements the production // of the same name. func (r *rdParser) parseColumnNameOrUserVarListOptWithBrackets() []*ast.ColumnNameOrUserVar { diff --git a/parser/parse_handler.go b/parser/parse_handler.go new file mode 100644 index 0000000..fc0aba4 --- /dev/null +++ b/parser/parse_handler.go @@ -0,0 +1,120 @@ +// 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 + +// The HANDLER statements (MySQL 26.7 §15.2.5). These postdate the +// goyacc grammar; the productions below are written from the MySQL 26.7 +// reference manual in the same style as parser.y. + +import ( + "github.com/sqlc-dev/marino/ast" +) + +func init() { + rdRegister(handler, (*rdParser).parseHandlerStmt) +} + +// parseHandlerStmt implements the HANDLER statement family: +// +// "HANDLER" TableName "OPEN" [["AS"] Identifier] +// | "HANDLER" TableName "READ" HandlerReadTail +// | "HANDLER" TableName "CLOSE" +func (r *rdParser) parseHandlerStmt() ast.StmtNode { + r.expect(handler) + table := r.parseTableName() + switch r.tok() { + case open: + r.advance() + stmt := &ast.HandlerOpenStmt{Table: table} + if r.accept(as) { + stmt.Alias = ast.NewCIStr(r.parseIdentifier()) + } else if isIdentifierTok(r.tok()) { + stmt.Alias = ast.NewCIStr(r.parseIdentifier()) + } + return stmt + case read: + r.advance() + return r.parseHandlerReadTail(table) + case close: + r.advance() + return &ast.HandlerCloseStmt{Table: table} + } + r.syntaxError() + return nil +} + +// parseHandlerReadTail implements the tail of HANDLER ... READ: +// +// Identifier HandlerCompareOp '(' ExpressionListOpt ')' HandlerReadTail2 +// | Identifier ("FIRST" | "NEXT" | "PREV" | "LAST") HandlerReadTail2 +// | ("FIRST" | "NEXT") HandlerReadTail2 +// +// with HandlerReadTail2: ["WHERE" Expression] [LimitClause]. An index +// name may also be "PRIMARY", naming the primary key. +func (r *rdParser) parseHandlerReadTail(table *ast.TableName) ast.StmtNode { + stmt := &ast.HandlerReadStmt{Table: table} + switch r.tok() { + case first: + r.advance() + stmt.Direction = ast.HandlerReadFirst + case next: + r.advance() + stmt.Direction = ast.HandlerReadNext + default: + stmt.IndexName = r.parseCacheIndexName() + switch r.tok() { + case first: + r.advance() + stmt.Direction = ast.HandlerReadFirst + case next: + r.advance() + stmt.Direction = ast.HandlerReadNext + case prev: + r.advance() + stmt.Direction = ast.HandlerReadPrev + case last: + r.advance() + stmt.Direction = ast.HandlerReadLast + case eq: + r.advance() + stmt.Op = ast.HandlerOpEQ + case le: + r.advance() + stmt.Op = ast.HandlerOpLE + case ge: + r.advance() + stmt.Op = ast.HandlerOpGE + case int('<'): + r.advance() + stmt.Op = ast.HandlerOpLT + case int('>'): + r.advance() + stmt.Op = ast.HandlerOpGT + default: + r.syntaxError() + } + if stmt.Op != ast.HandlerOpNone { + r.expect(int('(')) + stmt.Values = r.parseExpressionListOpt() + r.expect(int(')')) + } + } + if r.accept(where) { + stmt.Where = r.parseExpression() + } + if r.tok() == limit { + stmt.Limit = r.parseSelectStmtLimit() + } + return stmt +} diff --git a/parser/parse_select.go b/parser/parse_select.go index 636014a..8fdb3a4 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -256,16 +256,28 @@ func (r *rdParser) parseSelectStmt() *ast.SelectStmt { switch r.tok() { case selectKwd: st := r.parseSelectStmtBasic() + // MySQL also accepts the SelectStmtIntoOption between the field + // list and FROM (the trailing position is handled by + // parseSelectTail, which skips it once one is set). The INTO + // token bounds the last field's recorded text in that case. + fieldEnd := rdToken{offset: -1} + if r.tok() == into && st.Having == nil { + fieldEnd = *r.cur() + st.SelectIntoOpt = r.parseSelectStmtIntoOption() + } switch { case r.tok() == from && st.Having == nil && r.la(1) == dual: // SelectStmtFromDualTable: SelectStmtBasic FromDual // WhereClauseOptional, then SelectStmtGroup OrderByOptional ... fromTok := *r.cur() + if fieldEnd.offset < 0 { + fieldEnd = fromTok + } r.advance() r.expect(dual) lastField := st.Fields.Fields[len(st.Fields.Fields)-1] if lastField.Expr != nil && lastField.AsName.O == "" { - r.p.setNodeText(lastField, r.src[lastField.Offset:fromTok.offset-1]) + r.p.setNodeText(lastField, r.src[lastField.Offset:fieldEnd.offset-1]) } if r.accept(where) { st.Where = r.parseExpression() @@ -279,11 +291,14 @@ func (r *rdParser) parseSelectStmt() *ast.SelectStmt { // WhereClauseOptional SelectStmtGroup HavingClause // WindowClauseOptional, then OrderByOptional ... fromTok := *r.cur() + if fieldEnd.offset < 0 { + fieldEnd = fromTok + } r.advance() st.From = &ast.TableRefsClause{TableRefs: r.parseTableRefs()} lastField := st.Fields.Fields[len(st.Fields.Fields)-1] if lastField.Expr != nil && lastField.AsName.O == "" { - r.p.setNodeText(lastField, r.src[lastField.Offset:r.endOffsetAt(fromTok.offset)]) + r.p.setNodeText(lastField, r.src[lastField.Offset:r.endOffsetAt(fieldEnd.offset)]) } if r.accept(where) { st.Where = r.parseExpression() @@ -351,8 +366,10 @@ func (r *rdParser) parseSelectTail(st *ast.SelectStmt) { if lock := r.parseSelectLockOpt(); lock != nil { st.LockInfo = lock } - if opt := r.parseSelectStmtIntoOption(); opt != nil { - st.SelectIntoOpt = opt + if st.SelectIntoOpt == nil { + if opt := r.parseSelectStmtIntoOption(); opt != nil { + st.SelectIntoOpt = opt + } } } @@ -670,23 +687,55 @@ func (r *rdParser) parseSelectLockOpt() *ast.SelectLockInfo { return nil } -// parseSelectStmtIntoOption implements SelectStmtIntoOption. +// parseSelectStmtIntoOption implements SelectStmtIntoOption: +// +// "INTO" "OUTFILE" stringLit Fields Lines +// | "INTO" "DUMPFILE" stringLit +// | "INTO" IntoVar (',' IntoVar)* +// +// with IntoVar: UserVariable | Identifier (a stored program variable, +// only meaningful inside a routine body). func (r *rdParser) parseSelectStmtIntoOption() *ast.SelectIntoOption { if r.tok() != into { return nil } r.advance() - r.expect(outfile) - x := &ast.SelectIntoOption{Tp: ast.SelectIntoOutfile, FileName: r.expect(stringLit).lit} - if fields := r.parseFieldsClause(); fields != nil { - x.FieldsInfo = fields + switch r.tok() { + case outfile: + r.advance() + x := &ast.SelectIntoOption{Tp: ast.SelectIntoOutfile, FileName: r.expect(stringLit).lit} + if fields := r.parseFieldsClause(); fields != nil { + x.FieldsInfo = fields + } + if lines := r.parseLinesClause(); lines != nil { + x.LinesInfo = lines + } + return x + case dumpfile: + r.advance() + return &ast.SelectIntoOption{Tp: ast.SelectIntoDumpfile, FileName: r.expect(stringLit).lit} } - if lines := r.parseLinesClause(); lines != nil { - x.LinesInfo = lines + x := &ast.SelectIntoOption{Tp: ast.SelectIntoVars, Vars: []ast.ExprNode{r.parseSelectIntoVar()}} + for r.accept(int(',')) { + x.Vars = append(x.Vars, r.parseSelectIntoVar()) } return x } +// parseSelectIntoVar implements IntoVar. The variable expressions carry +// no origin position: SelectStmt.Accept does not traverse +// SelectIntoOpt, so a recorded position could never be normalized by +// visitors. +func (r *rdParser) parseSelectIntoVar() ast.ExprNode { + if r.tok() == singleAtIdentifier { + t := r.expect(singleAtIdentifier) + return &ast.VariableExpr{Name: strings.TrimPrefix(t.lit, "@")} + } + return &ast.ColumnNameExpr{ + Name: &ast.ColumnName{Name: ast.NewCIStr(r.parseIdentifier())}, + } +} + // parseFieldsClause implements Fields: FieldsOrColumns FieldItemList. func (r *rdParser) parseFieldsClause() *ast.FieldsClause { if r.tok() != fields && r.tok() != columns { diff --git a/parser/rd_parser.go b/parser/rd_parser.go index a983e1a..71742db 100644 --- a/parser/rd_parser.go +++ b/parser/rd_parser.go @@ -383,14 +383,20 @@ func (r *rdParser) parseStatement() ast.StmtNode { return r.parseLoadStatsStmt() case index: return r.parseLoadIndexStmt() + case xml: + return r.parseLoadXMLStmt() } r.unsupported("LOAD statement") return nil case importKwd: - if r.la(1) != into { - r.unsupported("IMPORT statement") + switch r.la(1) { + case into: + return r.parseImportIntoStmt() + case tableKwd: + return r.parseImportTableStmt() } - return r.parseImportIntoStmt() + r.unsupported("IMPORT statement") + return nil case batch: return r.parseNonTransactionalDMLStmt() default: diff --git a/parser/testdata/parser/mysql_dml/input.sql b/parser/testdata/parser/mysql_dml/input.sql new file mode 100644 index 0000000..fd5860b --- /dev/null +++ b/parser/testdata/parser/mysql_dml/input.sql @@ -0,0 +1,51 @@ +HANDLER t OPEN +-- case +HANDLER t OPEN AS h +-- case +HANDLER db1.t OPEN h +-- case +HANDLER t READ FIRST +-- case +HANDLER t READ NEXT +-- case +HANDLER t READ FIRST WHERE c > 1 LIMIT 10 +-- case +HANDLER t READ idx FIRST +-- case +HANDLER t READ idx NEXT +-- case +HANDLER t READ idx PREV +-- case +HANDLER t READ idx LAST +-- case +HANDLER t READ idx = (1, 'a') +-- case +HANDLER t READ idx <= (1) +-- case +HANDLER t READ idx >= (1) WHERE c < 100 +-- case +HANDLER t READ idx < (1) +-- case +HANDLER t READ idx > (1) LIMIT 2, 5 +-- case +HANDLER t READ PRIMARY = (1) +-- case +HANDLER t CLOSE +-- case +IMPORT TABLE FROM 't.sdi' +-- case +IMPORT TABLE FROM 't.sdi', 'u.sdi' +-- case +LOAD XML INFILE 'f.xml' INTO TABLE t +-- case +LOAD XML LOW_PRIORITY LOCAL INFILE 'f.xml' REPLACE INTO TABLE db1.t +-- case +LOAD XML CONCURRENT INFILE 'f.xml' IGNORE INTO TABLE t CHARACTER SET utf8mb4 ROWS IDENTIFIED BY '' IGNORE 2 LINES (a, @b) SET c = 1 +-- case +SELECT 1 INTO @a +-- case +SELECT a, b INTO @x, @y FROM t LIMIT 1 +-- case +SELECT 1 INTO DUMPFILE '/tmp/out' +-- case +SELECT prev, xml, dumpfile FROM concurrent diff --git a/parser/testdata/parser/mysql_dml/output.sql b/parser/testdata/parser/mysql_dml/output.sql new file mode 100644 index 0000000..25d1729 --- /dev/null +++ b/parser/testdata/parser/mysql_dml/output.sql @@ -0,0 +1,51 @@ +HANDLER `t` OPEN +-- case +HANDLER `t` OPEN AS `h` +-- case +HANDLER `db1`.`t` OPEN AS `h` +-- case +HANDLER `t` READ FIRST +-- case +HANDLER `t` READ NEXT +-- case +HANDLER `t` READ FIRST WHERE `c`>1 LIMIT 10 +-- case +HANDLER `t` READ `idx` FIRST +-- case +HANDLER `t` READ `idx` NEXT +-- case +HANDLER `t` READ `idx` PREV +-- case +HANDLER `t` READ `idx` LAST +-- case +HANDLER `t` READ `idx` = (1, _UTF8MB4'a') +-- case +HANDLER `t` READ `idx` <= (1) +-- case +HANDLER `t` READ `idx` >= (1) WHERE `c`<100 +-- case +HANDLER `t` READ `idx` < (1) +-- case +HANDLER `t` READ `idx` > (1) LIMIT 2,5 +-- case +HANDLER `t` READ `PRIMARY` = (1) +-- case +HANDLER `t` CLOSE +-- case +IMPORT TABLE FROM 't.sdi' +-- case +IMPORT TABLE FROM 't.sdi', 'u.sdi' +-- case +LOAD XML INFILE 'f.xml' INTO TABLE `t` +-- case +LOAD XML LOW_PRIORITY LOCAL INFILE 'f.xml' REPLACE INTO TABLE `db1`.`t` +-- case +LOAD XML CONCURRENT INFILE 'f.xml' IGNORE INTO TABLE `t` CHARACTER SET utf8mb4 ROWS IDENTIFIED BY '' IGNORE 2 ROWS (`a`,@`b`) SET `c`=1 +-- case +SELECT 1 INTO @`a` +-- case +SELECT `a`,`b` FROM `t` LIMIT 1 INTO @`x`, @`y` +-- case +SELECT 1 INTO DUMPFILE '/tmp/out' +-- case +SELECT `prev`,`xml`,`dumpfile` FROM `concurrent` diff --git a/parser/testdata/parser/mysql_unsupported_dml/input.sql b/parser/testdata/parser/mysql_unsupported_dml/input.sql deleted file mode 100644 index 32dd239..0000000 --- a/parser/testdata/parser/mysql_unsupported_dml/input.sql +++ /dev/null @@ -1,13 +0,0 @@ -HANDLER t OPEN --- case -HANDLER t READ FIRST --- case -HANDLER t READ NEXT --- case -HANDLER t CLOSE --- case -IMPORT TABLE FROM 't.sdi' --- case -LOAD XML INFILE 'f.xml' INTO TABLE t --- case -SELECT 1 INTO @a diff --git a/parser/testdata/parser/mysql_unsupported_dml/output.sql b/parser/testdata/parser/mysql_unsupported_dml/output.sql deleted file mode 100644 index f179149..0000000 --- a/parser/testdata/parser/mysql_unsupported_dml/output.sql +++ /dev/null @@ -1,13 +0,0 @@ --- error: line 1 column 7 near "HANDLER t OPEN" --- case --- error: line 1 column 7 near "HANDLER t READ FIRST" --- case --- error: line 1 column 7 near "HANDLER t READ NEXT" --- case --- error: line 1 column 7 near "HANDLER t CLOSE" --- case --- error: line 1 column 6 near "IMPORT TABLE FROM 't.sdi'" --- case --- error: line 1 column 4 near "LOAD XML INFILE 'f.xml' INTO TABLE t" --- case --- error: line 1 column 16 near "@a" diff --git a/parser/token_kinds.go b/parser/token_kinds.go index daaf253..2d0e877 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -205,6 +205,7 @@ const ( compressionLevel = 57662 compressionType = 57663 concurrency = 57664 + concurrent = 58292 config = 57665 connection = 57666 consistency = 57667 @@ -290,6 +291,7 @@ const ( dryRun = 58017 dual = 57416 dump = 58018 + dumpfile = 58293 duplicate = 57695 dynamic = 57696 elseIfKwd = 57418 @@ -664,6 +666,7 @@ const ( predicate = 58061 prepare = 57843 preserve = 57844 + prev = 58294 primary = 57519 primaryRegion = 58062 priority = 58063 @@ -995,6 +998,7 @@ const ( x509 = 57993 xa = 58284 xid = 58285 + xml = 58295 xor = 57593 yearMonth = 57594 yearType = 57994 From 375255fb48d3ec2ab22dcc74af3e16cea7461f61 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 21:46:21 +0000 Subject: [PATCH 4/4] Support the MySQL DDL statements for events, triggers, routines, and storage objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_ddl statement coverage group (MySQL 26.7 §15.1): its error goldens turn into Restore() goldens and the group is renamed to mysql_ddl with expanded coverage, following the mysql_admin precedent. Grammar, written from the reference manual since these statements postdate the goyacc grammar (parser/parse_mysql_ddl.go, with dispatch from the CREATE/ALTER/DROP statement heads): - CREATE/ALTER/DROP EVENT with the AT and EVERY ... STARTS/ENDS schedules, ON COMPLETION, ENABLE/DISABLE [ON REPLICA], COMMENT, RENAME TO, and DO bodies - CREATE/DROP TRIGGER with BEFORE/AFTER, INSERT/UPDATE/DELETE, FOR EACH ROW, and FOLLOWS/PRECEDES - CREATE FUNCTION for stored functions (parameter list, RETURNS type, characteristics, RETURN or compound bodies), ALTER FUNCTION, DROP FUNCTION, and ALTER PROCEDURE; RETURN joins ProcedureProcStmt; CREATE FUNCTION dispatches between the stored and loadable forms on the parameter list - The DEFINER = user clause on CREATE/ALTER VIEW-family, EVENT, TRIGGER, PROCEDURE (new ProcedureInfo.Definer field), and FUNCTION, with the statement head peeking past the clause to dispatch - ALTER VIEW, mirroring CreateViewStmt - CREATE/ALTER/DROP SERVER with the OPTIONS list - CREATE/ALTER/DROP [UNDO] TABLESPACE and CREATE/ALTER/DROP LOGFILE GROUP with a shared option catalogue (sizes, NODEGROUP, WAIT, ENCRYPTION, COMMENT, ENGINE, ENGINE_ATTRIBUTE) - CREATE/DROP SPATIAL REFERENCE SYSTEM with the NAME/DEFINITION/ ORGANIZATION/DESCRIPTION attributes; CREATE SPATIAL now dispatches between the index and SRS forms - CREATE/ALTER/DROP LIBRARY and CREATE/ALTER JSON DUALITY VIEW with the JSON_DUALITY_OBJECT('key' : value, ...) select-list constructor as a new expression atom - The remaining ALTER INSTANCE forms: ROTATE {INNODB|BINLOG} MASTER KEY, RELOAD TLS FOR CHANNEL, RELOAD KEYRING, and ENABLE/DISABLE INNODB REDO_LOG (new AlterInstanceStmt fields) - The USING (expr) spelling of CREATE MASKING POLICY (new Using field) and DROP MASKING POLICY Free-form words of these productions that are not keywords (REFERENCE, the SRS attribute and server option names, the size option names, ACTIVE/INACTIVE, KEYRING, REDO_LOG) are matched case-insensitively in identifier position, following the SIGNAL information-item precedent. New AST nodes (ast/mysql_ddl.go, plus DropMaskingPolicyStmt in ddl.go): the statements above with EventSchedule, TriggerOrder, RoutineCharacteristics, FunctionParam, ServerOption, TablespaceOption, SRSAttribute, and JSONDualityObjectExpr, plus their SEMCommand strings. Unlike ProcedureInfo, the new body-carrying nodes traverse their body statement in Accept. FieldType.Restore (not CompactStr) restores function parameter and return types so unspecified display widths round-trip. Keyword tables: DETERMINISTIC, EACH, MODIFIES, READS, RETURN, and UNDO become reserved words, AT, COMPLETION, CONTAINS, DATAFILE, DUALITY, ENDS, EVERY, FOLLOWS, INNODB, LOGFILE, OPTIONS, PRECEDES, ROTATE, SERVER, STARTS, UNDOFILE, and WRAPPER unreserved, and JSON_DUALITY_OBJECT a NotKeywordToken, matching their MySQL 26.7 classification; TestKeywordsLength counts updated accordingly and the lexer test for AT now expects the keyword token. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FGwevXQRPY7iPxud2UyaJH --- ast/ddl.go | 47 +- ast/misc.go | 33 + ast/mysql_ddl.go | 1833 +++++++++++++++++ ast/procedure.go | 14 +- ast/sem.go | 196 ++ parser/keyword_classes.go | 18 + parser/keywords.go | 23 + parser/keywords_test.go | 8 +- parser/lexer_test.go | 2 +- parser/misc.go | 24 + parser/parse_alter.go | 82 +- parser/parse_create_misc.go | 19 +- parser/parse_create_table.go | 52 +- parser/parse_drop.go | 71 + parser/parse_func.go | 19 + parser/parse_mysql_admin.go | 16 +- parser/parse_mysql_ddl.go | 889 ++++++++ parser/parse_procedure.go | 11 +- parser/testdata/parser/mysql_ddl/input.sql | 147 ++ parser/testdata/parser/mysql_ddl/output.sql | 147 ++ .../parser/mysql_unsupported_ddl/input.sql | 57 - .../parser/mysql_unsupported_ddl/output.sql | 57 - parser/token_kinds.go | 24 + 23 files changed, 3634 insertions(+), 155 deletions(-) create mode 100644 ast/mysql_ddl.go create mode 100644 parser/parse_mysql_ddl.go create mode 100644 parser/testdata/parser/mysql_ddl/input.sql create mode 100644 parser/testdata/parser/mysql_ddl/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_ddl/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_ddl/output.sql diff --git a/ast/ddl.go b/ast/ddl.go index aa87de3..ab75ccb 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -1853,6 +1853,9 @@ type CreateMaskingPolicyStmt struct { Expr ExprNode RestrictOps MaskingPolicyRestrictOps MaskingPolicyState MaskingPolicyState + // Using selects the USING (expr) spelling of the masking expression + // clause over AS expr. + Using bool } // Restore implements Node interface. @@ -1875,9 +1878,18 @@ func (n *CreateMaskingPolicyStmt) Restore(ctx *format.RestoreCtx) error { return annotate(err, "An error occurred while restore CreateMaskingPolicyStmt.Column") } ctx.WritePlain(") ") - ctx.WriteKeyWord("AS ") - if err := n.Expr.Restore(ctx); err != nil { - return annotate(err, "An error occurred while restore CreateMaskingPolicyStmt.Expr") + if n.Using { + ctx.WriteKeyWord("USING ") + ctx.WritePlain("(") + if err := n.Expr.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateMaskingPolicyStmt.Expr") + } + ctx.WritePlain(")") + } else { + ctx.WriteKeyWord("AS ") + if err := n.Expr.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateMaskingPolicyStmt.Expr") + } } if n.RestrictOps != MaskingPolicyRestrictOpNone { ctx.WritePlain(" ") @@ -1925,6 +1937,35 @@ func (n *CreateMaskingPolicyStmt) Accept(v Visitor) (Node, bool) { return v.Leave(n) } +// DropMaskingPolicyStmt is a statement to drop a masking policy: +// DROP MASKING POLICY [IF EXISTS] policy_name. +type DropMaskingPolicyStmt struct { + ddlNode + + IfExists bool + PolicyName CIStr +} + +// Restore implements Node interface. +func (n *DropMaskingPolicyStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP MASKING POLICY ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + ctx.WriteName(n.PolicyName.O) + return nil +} + +// Accept implements Node Accept interface. +func (n *DropMaskingPolicyStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropMaskingPolicyStmt) + return v.Leave(n) +} + // CreateResourceGroupStmt is a statement to create a policy. type CreateResourceGroupStmt struct { ddlNode diff --git a/ast/misc.go b/ast/misc.go index 4c08bd1..8514af0 100644 --- a/ast/misc.go +++ b/ast/misc.go @@ -2067,18 +2067,51 @@ func (n *AlterUserStmt) Accept(v Visitor) (Node, bool) { // AlterInstanceStmt modifies instance. // See https://dev.mysql.com/doc/refman/8.0/en/alter-instance.html +// Exactly one of the action fields is set: ReloadTLS (with optional +// Channel and NoRollbackOnError), RotateInnoDBMasterKey, +// RotateBinlogMasterKey, ReloadKeyring, EnableInnoDBRedoLog, or +// DisableInnoDBRedoLog. type AlterInstanceStmt struct { stmtNode ReloadTLS bool NoRollbackOnError bool + // Channel is the FOR CHANNEL name of RELOAD TLS (mysql_main or + // mysql_admin); empty when absent. + Channel string + RotateInnoDBMasterKey bool + RotateBinlogMasterKey bool + ReloadKeyring bool + EnableInnoDBRedoLog bool + DisableInnoDBRedoLog bool } // Restore implements Node interface. func (n *AlterInstanceStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("ALTER INSTANCE") + switch { + case n.RotateInnoDBMasterKey: + ctx.WriteKeyWord(" ROTATE INNODB MASTER KEY") + return nil + case n.RotateBinlogMasterKey: + ctx.WriteKeyWord(" ROTATE BINLOG MASTER KEY") + return nil + case n.ReloadKeyring: + ctx.WriteKeyWord(" RELOAD KEYRING") + return nil + case n.EnableInnoDBRedoLog: + ctx.WriteKeyWord(" ENABLE INNODB REDO_LOG") + return nil + case n.DisableInnoDBRedoLog: + ctx.WriteKeyWord(" DISABLE INNODB REDO_LOG") + return nil + } if n.ReloadTLS { ctx.WriteKeyWord(" RELOAD TLS") + if n.Channel != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteName(n.Channel) + } } if n.NoRollbackOnError { ctx.WriteKeyWord(" NO ROLLBACK ON ERROR") diff --git a/ast/mysql_ddl.go b/ast/mysql_ddl.go new file mode 100644 index 0000000..666c229 --- /dev/null +++ b/ast/mysql_ddl.go @@ -0,0 +1,1833 @@ +// 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 ast + +import ( + "io" + + "github.com/sqlc-dev/marino/auth" + "github.com/sqlc-dev/marino/format" + "github.com/sqlc-dev/marino/types" +) + +// The MySQL data definition statements that postdate the goyacc grammar +// (MySQL 26.7 §15.1): events, triggers, stored functions (and the ALTER +// forms of stored routines), ALTER VIEW, servers, tablespaces, logfile +// groups, spatial reference systems, libraries, and JSON duality views. +// (The masking policy statements live in ddl.go with the other masking +// nodes, and AlterInstanceStmt in misc.go.) + +var ( + _ ExprNode = &JSONDualityObjectExpr{} + + _ DDLNode = &CreateEventStmt{} + _ DDLNode = &AlterEventStmt{} + _ DDLNode = &DropEventStmt{} + _ DDLNode = &CreateTriggerStmt{} + _ DDLNode = &DropTriggerStmt{} + _ DDLNode = &CreateFunctionStmt{} + _ DDLNode = &AlterFunctionStmt{} + _ DDLNode = &DropFunctionStmt{} + _ DDLNode = &AlterProcedureStmt{} + _ DDLNode = &AlterViewStmt{} + _ DDLNode = &CreateServerStmt{} + _ DDLNode = &AlterServerStmt{} + _ DDLNode = &DropServerStmt{} + _ DDLNode = &CreateTablespaceStmt{} + _ DDLNode = &AlterTablespaceStmt{} + _ DDLNode = &DropTablespaceStmt{} + _ DDLNode = &CreateLogfileGroupStmt{} + _ DDLNode = &AlterLogfileGroupStmt{} + _ DDLNode = &DropLogfileGroupStmt{} + _ DDLNode = &CreateSpatialReferenceSystemStmt{} + _ DDLNode = &DropSpatialReferenceSystemStmt{} + _ DDLNode = &CreateLibraryStmt{} + _ DDLNode = &AlterLibraryStmt{} + _ DDLNode = &DropLibraryStmt{} + _ DDLNode = &CreateJSONDualityViewStmt{} + _ DDLNode = &AlterJSONDualityViewStmt{} + + _ StmtNode = &ReturnStmt{} +) + +// restoreDefiner writes a DEFINER = user clause followed by one space. +func restoreDefiner(ctx *format.RestoreCtx, definer *auth.UserIdentity, parent string) error { + ctx.WriteKeyWord("DEFINER ") + ctx.WritePlain("= ") + if err := definer.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore %s.Definer", parent) + } + ctx.WritePlain(" ") + return nil +} + +/* + * Events + */ + +// EventSchedule is the ON SCHEDULE clause of CREATE/ALTER EVENT: +// +// AT timestamp +// | EVERY interval [STARTS timestamp] [ENDS timestamp] +// +// At is set for the AT form; otherwise Every holds the interval quantity +// and Unit its time unit, with optional Starts and Ends timestamps. +type EventSchedule struct { + At ExprNode + Every ExprNode + Unit TimeUnitType + Starts ExprNode + Ends ExprNode +} + +// Restore implements Node interface. +func (n *EventSchedule) Restore(ctx *format.RestoreCtx) error { + if n.At != nil { + ctx.WriteKeyWord("AT ") + if err := n.At.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore EventSchedule.At") + } + return nil + } + ctx.WriteKeyWord("EVERY ") + if err := n.Every.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore EventSchedule.Every") + } + ctx.WritePlain(" ") + ctx.WriteKeyWord(n.Unit.String()) + if n.Starts != nil { + ctx.WriteKeyWord(" STARTS ") + if err := n.Starts.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore EventSchedule.Starts") + } + } + if n.Ends != nil { + ctx.WriteKeyWord(" ENDS ") + if err := n.Ends.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore EventSchedule.Ends") + } + } + return nil +} + +func (n *EventSchedule) accept(v Visitor) bool { + for _, e := range []*ExprNode{&n.At, &n.Every, &n.Starts, &n.Ends} { + if *e == nil { + continue + } + node, ok := (*e).Accept(v) + if !ok { + return false + } + *e = node.(ExprNode) + } + return true +} + +// EventCompletion is the ON COMPLETION clause of CREATE/ALTER EVENT. +type EventCompletion int + +const ( + // EventCompletionDefault omits the clause. + EventCompletionDefault EventCompletion = iota + // EventCompletionPreserve is ON COMPLETION PRESERVE. + EventCompletionPreserve + // EventCompletionNotPreserve is ON COMPLETION NOT PRESERVE. + EventCompletionNotPreserve +) + +// EventState is the ENABLE/DISABLE clause of CREATE/ALTER EVENT. +type EventState int + +const ( + // EventStateDefault omits the clause. + EventStateDefault EventState = iota + // EventStateEnable is ENABLE. + EventStateEnable + // EventStateDisable is DISABLE. + EventStateDisable + // EventStateDisableOnReplica is DISABLE ON REPLICA. + EventStateDisableOnReplica +) + +func restoreEventState(ctx *format.RestoreCtx, state EventState) { + switch state { + case EventStateEnable: + ctx.WriteKeyWord(" ENABLE") + case EventStateDisable: + ctx.WriteKeyWord(" DISABLE") + case EventStateDisableOnReplica: + ctx.WriteKeyWord(" DISABLE ON REPLICA") + } +} + +func restoreEventCompletion(ctx *format.RestoreCtx, completion EventCompletion) { + switch completion { + case EventCompletionPreserve: + ctx.WriteKeyWord(" ON COMPLETION PRESERVE") + case EventCompletionNotPreserve: + ctx.WriteKeyWord(" ON COMPLETION NOT PRESERVE") + } +} + +// CreateEventStmt is a CREATE EVENT statement: +// +// CREATE [DEFINER = user] EVENT [IF NOT EXISTS] event_name +// ON SCHEDULE schedule [ON COMPLETION [NOT] PRESERVE] +// [ENABLE | DISABLE | DISABLE ON REPLICA] [COMMENT 'string'] +// DO event_body +type CreateEventStmt struct { + ddlNode + + IfNotExists bool + Definer *auth.UserIdentity // nil when absent + EventName *TableName + Schedule *EventSchedule + Completion EventCompletion + State EventState + Comment *string + Body StmtNode +} + +// Restore implements Node interface. +func (n *CreateEventStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.Definer != nil { + if err := restoreDefiner(ctx, n.Definer, "CreateEventStmt"); err != nil { + return err + } + } + ctx.WriteKeyWord("EVENT ") + if n.IfNotExists { + ctx.WriteKeyWord("IF NOT EXISTS ") + } + if err := n.EventName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateEventStmt.EventName") + } + ctx.WriteKeyWord(" ON SCHEDULE ") + if err := n.Schedule.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateEventStmt.Schedule") + } + restoreEventCompletion(ctx, n.Completion) + restoreEventState(ctx, n.State) + if n.Comment != nil { + ctx.WriteKeyWord(" COMMENT ") + ctx.WriteString(*n.Comment) + } + ctx.WriteKeyWord(" DO ") + if err := n.Body.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateEventStmt.Body") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateEventStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateEventStmt) + node, ok := n.EventName.Accept(v) + if !ok { + return n, false + } + n.EventName = node.(*TableName) + if !n.Schedule.accept(v) { + return n, false + } + if n.Body != nil { + node, ok := n.Body.Accept(v) + if !ok { + return n, false + } + n.Body = node.(StmtNode) + } + return v.Leave(n) +} + +// AlterEventStmt is an ALTER EVENT statement: +// +// ALTER [DEFINER = user] EVENT event_name +// [ON SCHEDULE schedule] [ON COMPLETION [NOT] PRESERVE] +// [RENAME TO new_event_name] +// [ENABLE | DISABLE | DISABLE ON REPLICA] [COMMENT 'string'] +// [DO event_body] +type AlterEventStmt struct { + ddlNode + + Definer *auth.UserIdentity // nil when absent + EventName *TableName + Schedule *EventSchedule // nil when absent + Completion EventCompletion + RenameTo *TableName + State EventState + Comment *string + Body StmtNode // nil when absent +} + +// Restore implements Node interface. +func (n *AlterEventStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER ") + if n.Definer != nil { + if err := restoreDefiner(ctx, n.Definer, "AlterEventStmt"); err != nil { + return err + } + } + ctx.WriteKeyWord("EVENT ") + if err := n.EventName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterEventStmt.EventName") + } + if n.Schedule != nil { + ctx.WriteKeyWord(" ON SCHEDULE ") + if err := n.Schedule.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterEventStmt.Schedule") + } + } + restoreEventCompletion(ctx, n.Completion) + if n.RenameTo != nil { + ctx.WriteKeyWord(" RENAME TO ") + if err := n.RenameTo.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterEventStmt.RenameTo") + } + } + restoreEventState(ctx, n.State) + if n.Comment != nil { + ctx.WriteKeyWord(" COMMENT ") + ctx.WriteString(*n.Comment) + } + if n.Body != nil { + ctx.WriteKeyWord(" DO ") + if err := n.Body.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterEventStmt.Body") + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *AlterEventStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterEventStmt) + node, ok := n.EventName.Accept(v) + if !ok { + return n, false + } + n.EventName = node.(*TableName) + if n.Schedule != nil && !n.Schedule.accept(v) { + return n, false + } + if n.RenameTo != nil { + node, ok := n.RenameTo.Accept(v) + if !ok { + return n, false + } + n.RenameTo = node.(*TableName) + } + if n.Body != nil { + node, ok := n.Body.Accept(v) + if !ok { + return n, false + } + n.Body = node.(StmtNode) + } + return v.Leave(n) +} + +// DropEventStmt is a DROP EVENT statement: +// DROP EVENT [IF EXISTS] event_name. +type DropEventStmt struct { + ddlNode + + IfExists bool + EventName *TableName +} + +// Restore implements Node interface. +func (n *DropEventStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP EVENT ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + if err := n.EventName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore DropEventStmt.EventName") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *DropEventStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropEventStmt) + node, ok := n.EventName.Accept(v) + if !ok { + return n, false + } + n.EventName = node.(*TableName) + return v.Leave(n) +} + +/* + * Triggers + */ + +// TriggerTime is the BEFORE/AFTER modifier of CREATE TRIGGER. +type TriggerTime int + +const ( + // TriggerTimeBefore is BEFORE. + TriggerTimeBefore TriggerTime = iota + // TriggerTimeAfter is AFTER. + TriggerTimeAfter +) + +// String implements fmt.Stringer interface. +func (n TriggerTime) String() string { + switch n { + case TriggerTimeBefore: + return "BEFORE" + case TriggerTimeAfter: + return "AFTER" + } + return "" +} + +// TriggerEvent is the INSERT/UPDATE/DELETE event of CREATE TRIGGER. +type TriggerEvent int + +const ( + // TriggerEventInsert is INSERT. + TriggerEventInsert TriggerEvent = iota + // TriggerEventUpdate is UPDATE. + TriggerEventUpdate + // TriggerEventDelete is DELETE. + TriggerEventDelete +) + +// String implements fmt.Stringer interface. +func (n TriggerEvent) String() string { + switch n { + case TriggerEventInsert: + return "INSERT" + case TriggerEventUpdate: + return "UPDATE" + case TriggerEventDelete: + return "DELETE" + } + return "" +} + +// TriggerOrder is the FOLLOWS/PRECEDES clause of CREATE TRIGGER. +type TriggerOrder struct { + Precedes bool // FOLLOWS when false + OtherTrigger CIStr +} + +// CreateTriggerStmt is a CREATE TRIGGER statement: +// +// CREATE [DEFINER = user] TRIGGER [IF NOT EXISTS] trigger_name +// {BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON tbl_name +// FOR EACH ROW [{FOLLOWS | PRECEDES} other_trigger_name] +// trigger_body +type CreateTriggerStmt struct { + ddlNode + + IfNotExists bool + Definer *auth.UserIdentity // nil when absent + TriggerName *TableName + TriggerTime TriggerTime + TriggerEvent TriggerEvent + Table *TableName + Order *TriggerOrder // nil when absent + Body StmtNode +} + +// Restore implements Node interface. +func (n *CreateTriggerStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.Definer != nil { + if err := restoreDefiner(ctx, n.Definer, "CreateTriggerStmt"); err != nil { + return err + } + } + ctx.WriteKeyWord("TRIGGER ") + if n.IfNotExists { + ctx.WriteKeyWord("IF NOT EXISTS ") + } + if err := n.TriggerName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateTriggerStmt.TriggerName") + } + ctx.WritePlain(" ") + ctx.WriteKeyWord(n.TriggerTime.String()) + ctx.WritePlain(" ") + ctx.WriteKeyWord(n.TriggerEvent.String()) + ctx.WriteKeyWord(" ON ") + if err := n.Table.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateTriggerStmt.Table") + } + ctx.WriteKeyWord(" FOR EACH ROW") + if n.Order != nil { + if n.Order.Precedes { + ctx.WriteKeyWord(" PRECEDES ") + } else { + ctx.WriteKeyWord(" FOLLOWS ") + } + ctx.WriteName(n.Order.OtherTrigger.O) + } + ctx.WritePlain(" ") + if err := n.Body.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateTriggerStmt.Body") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateTriggerStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateTriggerStmt) + node, ok := n.TriggerName.Accept(v) + if !ok { + return n, false + } + n.TriggerName = node.(*TableName) + node, ok = n.Table.Accept(v) + if !ok { + return n, false + } + n.Table = node.(*TableName) + if n.Body != nil { + node, ok := n.Body.Accept(v) + if !ok { + return n, false + } + n.Body = node.(StmtNode) + } + return v.Leave(n) +} + +// DropTriggerStmt is a DROP TRIGGER statement: +// DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name. +type DropTriggerStmt struct { + ddlNode + + IfExists bool + TriggerName *TableName +} + +// Restore implements Node interface. +func (n *DropTriggerStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP TRIGGER ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + if err := n.TriggerName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore DropTriggerStmt.TriggerName") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *DropTriggerStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropTriggerStmt) + node, ok := n.TriggerName.Accept(v) + if !ok { + return n, false + } + n.TriggerName = node.(*TableName) + return v.Leave(n) +} + +/* + * Stored routines + */ + +// RoutineDeterminism is the [NOT] DETERMINISTIC characteristic of a +// stored routine. +type RoutineDeterminism int + +const ( + // RoutineDeterminismDefault omits the characteristic. + RoutineDeterminismDefault RoutineDeterminism = iota + // RoutineDeterministic is DETERMINISTIC. + RoutineDeterministic + // RoutineNotDeterministic is NOT DETERMINISTIC. + RoutineNotDeterministic +) + +// RoutineDataAccess is the SQL data access characteristic of a stored +// routine. +type RoutineDataAccess int + +const ( + // RoutineDataAccessDefault omits the characteristic. + RoutineDataAccessDefault RoutineDataAccess = iota + // RoutineContainsSQL is CONTAINS SQL. + RoutineContainsSQL + // RoutineNoSQL is NO SQL. + RoutineNoSQL + // RoutineReadsSQLData is READS SQL DATA. + RoutineReadsSQLData + // RoutineModifiesSQLData is MODIFIES SQL DATA. + RoutineModifiesSQLData +) + +// RoutineCharacteristics are the characteristics of a stored routine. +// They restore in the canonical order COMMENT, LANGUAGE, [NOT] +// DETERMINISTIC, data access, SQL SECURITY regardless of source order. +type RoutineCharacteristics struct { + Comment *string + Language CIStr // empty when absent (SQL, JAVASCRIPT, ...) + Determinism RoutineDeterminism + DataAccess RoutineDataAccess + Security *ViewSecurity // nil when absent +} + +// Restore implements Node interface. Every present characteristic is +// written with one leading space. +func (n *RoutineCharacteristics) Restore(ctx *format.RestoreCtx) error { + if n.Comment != nil { + ctx.WriteKeyWord(" COMMENT ") + ctx.WriteString(*n.Comment) + } + if n.Language.O != "" { + ctx.WriteKeyWord(" LANGUAGE ") + ctx.WriteKeyWord(n.Language.O) + } + switch n.Determinism { + case RoutineDeterministic: + ctx.WriteKeyWord(" DETERMINISTIC") + case RoutineNotDeterministic: + ctx.WriteKeyWord(" NOT DETERMINISTIC") + } + switch n.DataAccess { + case RoutineContainsSQL: + ctx.WriteKeyWord(" CONTAINS SQL") + case RoutineNoSQL: + ctx.WriteKeyWord(" NO SQL") + case RoutineReadsSQLData: + ctx.WriteKeyWord(" READS SQL DATA") + case RoutineModifiesSQLData: + ctx.WriteKeyWord(" MODIFIES SQL DATA") + } + if n.Security != nil { + ctx.WriteKeyWord(" SQL SECURITY ") + if *n.Security == SecurityInvoker { + ctx.WriteKeyWord("INVOKER") + } else { + ctx.WriteKeyWord("DEFINER") + } + } + return nil +} + +// FunctionParam is one parameter of a stored function; unlike procedure +// parameters (StoreParameter) it has no IN/OUT/INOUT mode. +type FunctionParam struct { + ParamName string + ParamType *types.FieldType +} + +// Restore implements Node interface. +func (n *FunctionParam) Restore(ctx *format.RestoreCtx) error { + ctx.WriteName(n.ParamName) + ctx.WritePlain(" ") + if err := n.ParamType.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore FunctionParam.ParamType") + } + return nil +} + +// CreateFunctionStmt is a CREATE FUNCTION statement for stored +// functions (CreateLoadableFunctionStmt is the loadable function form): +// +// CREATE [DEFINER = user] FUNCTION [IF NOT EXISTS] sp_name +// ([func_parameter[, ...]]) RETURNS type [characteristic ...] +// routine_body +type CreateFunctionStmt struct { + ddlNode + + IfNotExists bool + Definer *auth.UserIdentity // nil when absent + FunctionName *TableName + Params []*FunctionParam + ReturnType *types.FieldType + Characteristics RoutineCharacteristics + Body StmtNode +} + +// Restore implements Node interface. +func (n *CreateFunctionStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.Definer != nil { + if err := restoreDefiner(ctx, n.Definer, "CreateFunctionStmt"); err != nil { + return err + } + } + ctx.WriteKeyWord("FUNCTION ") + if n.IfNotExists { + ctx.WriteKeyWord("IF NOT EXISTS ") + } + if err := n.FunctionName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateFunctionStmt.FunctionName") + } + ctx.WritePlain("(") + for i, param := range n.Params { + if i != 0 { + ctx.WritePlain(", ") + } + if err := param.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore CreateFunctionStmt.Params[%d]", i) + } + } + ctx.WritePlain(")") + ctx.WriteKeyWord(" RETURNS ") + if err := n.ReturnType.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateFunctionStmt.ReturnType") + } + if err := n.Characteristics.Restore(ctx); err != nil { + return err + } + ctx.WritePlain(" ") + if err := n.Body.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateFunctionStmt.Body") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateFunctionStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateFunctionStmt) + node, ok := n.FunctionName.Accept(v) + if !ok { + return n, false + } + n.FunctionName = node.(*TableName) + if n.Body != nil { + node, ok := n.Body.Accept(v) + if !ok { + return n, false + } + n.Body = node.(StmtNode) + } + return v.Leave(n) +} + +// AlterFunctionStmt is an ALTER FUNCTION statement: +// ALTER FUNCTION func_name [characteristic ...]. +type AlterFunctionStmt struct { + ddlNode + + FunctionName *TableName + Characteristics RoutineCharacteristics +} + +// Restore implements Node interface. +func (n *AlterFunctionStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER FUNCTION ") + if err := n.FunctionName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterFunctionStmt.FunctionName") + } + return n.Characteristics.Restore(ctx) +} + +// Accept implements Node Accept interface. +func (n *AlterFunctionStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterFunctionStmt) + node, ok := n.FunctionName.Accept(v) + if !ok { + return n, false + } + n.FunctionName = node.(*TableName) + return v.Leave(n) +} + +// DropFunctionStmt is a DROP FUNCTION statement (for a stored or +// loadable function): DROP FUNCTION [IF EXISTS] func_name. +type DropFunctionStmt struct { + ddlNode + + IfExists bool + FunctionName *TableName +} + +// Restore implements Node interface. +func (n *DropFunctionStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP FUNCTION ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + if err := n.FunctionName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore DropFunctionStmt.FunctionName") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *DropFunctionStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropFunctionStmt) + node, ok := n.FunctionName.Accept(v) + if !ok { + return n, false + } + n.FunctionName = node.(*TableName) + return v.Leave(n) +} + +// AlterProcedureStmt is an ALTER PROCEDURE statement: +// ALTER PROCEDURE proc_name [characteristic ...]. +type AlterProcedureStmt struct { + ddlNode + + ProcedureName *TableName + Characteristics RoutineCharacteristics +} + +// Restore implements Node interface. +func (n *AlterProcedureStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER PROCEDURE ") + if err := n.ProcedureName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterProcedureStmt.ProcedureName") + } + return n.Characteristics.Restore(ctx) +} + +// Accept implements Node Accept interface. +func (n *AlterProcedureStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterProcedureStmt) + node, ok := n.ProcedureName.Accept(v) + if !ok { + return n, false + } + n.ProcedureName = node.(*TableName) + return v.Leave(n) +} + +// ReturnStmt is a RETURN statement in a stored function body: +// RETURN expr. +type ReturnStmt struct { + stmtNode + + Expr ExprNode +} + +// Restore implements Node interface. +func (n *ReturnStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("RETURN ") + if err := n.Expr.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ReturnStmt.Expr") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *ReturnStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ReturnStmt) + node, ok := n.Expr.Accept(v) + if !ok { + return n, false + } + n.Expr = node.(ExprNode) + return v.Leave(n) +} + +/* + * ALTER VIEW + */ + +// AlterViewStmt is an ALTER VIEW statement: +// +// ALTER [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] +// [DEFINER = user] [SQL SECURITY {DEFINER | INVOKER}] +// VIEW view_name [(column_list)] AS select_statement +// [WITH [CASCADED | LOCAL] CHECK OPTION] +// +// The fields mirror CreateViewStmt: Definer defaults to CURRENT_USER +// and CheckOption to CASCADED when their clauses are absent. +type AlterViewStmt struct { + ddlNode + + ViewName *TableName + Cols []CIStr + Select StmtNode + Algorithm ViewAlgorithm + Definer *auth.UserIdentity + Security ViewSecurity + CheckOption ViewCheckOption +} + +// Restore implements Node interface. +func (n *AlterViewStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER ") + ctx.WriteKeyWord("ALGORITHM ") + ctx.WritePlainf("= %s ", n.Algorithm.String()) + if err := restoreDefiner(ctx, n.Definer, "AlterViewStmt"); err != nil { + return err + } + ctx.WriteKeyWord("SQL SECURITY ") + ctx.WriteKeyWord(n.Security.String()) + ctx.WriteKeyWord(" VIEW ") + if err := n.ViewName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterViewStmt.ViewName") + } + for i, col := range n.Cols { + if i == 0 { + ctx.WritePlain(" (") + } else { + ctx.WritePlain(",") + } + ctx.WriteName(col.O) + if i == len(n.Cols)-1 { + ctx.WritePlain(")") + } + } + ctx.WriteKeyWord(" AS ") + if err := n.Select.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterViewStmt.Select") + } + if n.CheckOption != CheckOptionCascaded { + ctx.WriteKeyWord(" WITH ") + ctx.WriteKeyWord(n.CheckOption.String()) + ctx.WriteKeyWord(" CHECK OPTION") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *AlterViewStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterViewStmt) + node, ok := n.ViewName.Accept(v) + if !ok { + return n, false + } + n.ViewName = node.(*TableName) + selnode, ok := n.Select.Accept(v) + if !ok { + return n, false + } + n.Select = selnode.(StmtNode) + return v.Leave(n) +} + +/* + * Servers + */ + +// ServerOption is one option of CREATE/ALTER SERVER: +// {HOST | DATABASE | USER | PASSWORD | SOCKET | OWNER} 'string' +// | PORT port_num. Names are stored uppercase. +type ServerOption struct { + Name string + Value ValueExpr +} + +// Restore implements Node interface. +func (n *ServerOption) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord(n.Name) + ctx.WritePlain(" ") + if err := n.Value.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ServerOption.Value") + } + return nil +} + +// CreateServerStmt is a CREATE SERVER statement: +// +// CREATE SERVER server_name FOREIGN DATA WRAPPER wrapper_name +// OPTIONS (option [, option] ...) +type CreateServerStmt struct { + ddlNode + + ServerName CIStr + Wrapper CIStr + Options []*ServerOption +} + +// Restore implements Node interface. +func (n *CreateServerStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE SERVER ") + ctx.WriteName(n.ServerName.O) + ctx.WriteKeyWord(" FOREIGN DATA WRAPPER ") + ctx.WriteName(n.Wrapper.O) + ctx.WriteKeyWord(" OPTIONS ") + ctx.WritePlain("(") + for i, opt := range n.Options { + if i != 0 { + ctx.WritePlain(", ") + } + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore CreateServerStmt.Options[%d]", i) + } + } + ctx.WritePlain(")") + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateServerStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateServerStmt) + return v.Leave(n) +} + +// AlterServerStmt is an ALTER SERVER statement: +// ALTER SERVER server_name OPTIONS (option [, option] ...). +type AlterServerStmt struct { + ddlNode + + ServerName CIStr + Options []*ServerOption +} + +// Restore implements Node interface. +func (n *AlterServerStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER SERVER ") + ctx.WriteName(n.ServerName.O) + ctx.WriteKeyWord(" OPTIONS ") + ctx.WritePlain("(") + for i, opt := range n.Options { + if i != 0 { + ctx.WritePlain(", ") + } + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore AlterServerStmt.Options[%d]", i) + } + } + ctx.WritePlain(")") + return nil +} + +// Accept implements Node Accept interface. +func (n *AlterServerStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterServerStmt) + return v.Leave(n) +} + +// DropServerStmt is a DROP SERVER statement: +// DROP SERVER [IF EXISTS] server_name. +type DropServerStmt struct { + ddlNode + + IfExists bool + ServerName CIStr +} + +// Restore implements Node interface. +func (n *DropServerStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP SERVER ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + ctx.WriteName(n.ServerName.O) + return nil +} + +// Accept implements Node Accept interface. +func (n *DropServerStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropServerStmt) + return v.Leave(n) +} + +/* + * Tablespaces and logfile groups + */ + +// TablespaceOptionType names one option of the CREATE/ALTER/DROP +// TABLESPACE and CREATE/ALTER/DROP LOGFILE GROUP statements. +type TablespaceOptionType int + +const ( + // TablespaceOptionInitialSize is INITIAL_SIZE. + TablespaceOptionInitialSize TablespaceOptionType = iota + // TablespaceOptionAutoextendSize is AUTOEXTEND_SIZE. + TablespaceOptionAutoextendSize + // TablespaceOptionMaxSize is MAX_SIZE. + TablespaceOptionMaxSize + // TablespaceOptionExtentSize is EXTENT_SIZE. + TablespaceOptionExtentSize + // TablespaceOptionNodegroup is NODEGROUP. + TablespaceOptionNodegroup + // TablespaceOptionFileBlockSize is FILE_BLOCK_SIZE. + TablespaceOptionFileBlockSize + // TablespaceOptionUndoBufferSize is UNDO_BUFFER_SIZE. + TablespaceOptionUndoBufferSize + // TablespaceOptionRedoBufferSize is REDO_BUFFER_SIZE. + TablespaceOptionRedoBufferSize + // TablespaceOptionWait is WAIT. + TablespaceOptionWait + // TablespaceOptionEncryption is ENCRYPTION. + TablespaceOptionEncryption + // TablespaceOptionComment is COMMENT. + TablespaceOptionComment + // TablespaceOptionEngine is ENGINE. + TablespaceOptionEngine + // TablespaceOptionEngineAttribute is ENGINE_ATTRIBUTE. + TablespaceOptionEngineAttribute +) + +// String implements fmt.Stringer interface. +func (n TablespaceOptionType) String() string { + switch n { + case TablespaceOptionInitialSize: + return "INITIAL_SIZE" + case TablespaceOptionAutoextendSize: + return "AUTOEXTEND_SIZE" + case TablespaceOptionMaxSize: + return "MAX_SIZE" + case TablespaceOptionExtentSize: + return "EXTENT_SIZE" + case TablespaceOptionNodegroup: + return "NODEGROUP" + case TablespaceOptionFileBlockSize: + return "FILE_BLOCK_SIZE" + case TablespaceOptionUndoBufferSize: + return "UNDO_BUFFER_SIZE" + case TablespaceOptionRedoBufferSize: + return "REDO_BUFFER_SIZE" + case TablespaceOptionWait: + return "WAIT" + case TablespaceOptionEncryption: + return "ENCRYPTION" + case TablespaceOptionComment: + return "COMMENT" + case TablespaceOptionEngine: + return "ENGINE" + case TablespaceOptionEngineAttribute: + return "ENGINE_ATTRIBUTE" + } + return "" +} + +// TablespaceOption is one option of a tablespace or logfile group +// statement. The size options, NODEGROUP, and FILE_BLOCK_SIZE use +// UintValue; ENCRYPTION, COMMENT, and ENGINE_ATTRIBUTE use StrValue +// restored as a string literal; ENGINE uses StrValue restored as a +// plain word; WAIT uses neither. +type TablespaceOption struct { + Tp TablespaceOptionType + StrValue string + UintValue uint64 +} + +// Restore implements Node interface. +func (n *TablespaceOption) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord(n.Tp.String()) + switch n.Tp { + case TablespaceOptionWait: + case TablespaceOptionEngine: + ctx.WritePlain(" = ") + ctx.WriteKeyWord(n.StrValue) + case TablespaceOptionEncryption, TablespaceOptionComment, TablespaceOptionEngineAttribute: + ctx.WritePlain(" = ") + ctx.WriteString(n.StrValue) + default: + ctx.WritePlainf(" = %d", n.UintValue) + } + return nil +} + +func restoreTablespaceOptions(ctx *format.RestoreCtx, options []*TablespaceOption, parent string) error { + for i, opt := range options { + ctx.WritePlain(" ") + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore %s.Options[%d]", parent, i) + } + } + return nil +} + +// CreateTablespaceStmt is a CREATE TABLESPACE statement: +// +// CREATE [UNDO] TABLESPACE tablespace_name [ADD DATAFILE 'file_name'] +// [USE LOGFILE GROUP logfile_group] [option ...] +type CreateTablespaceStmt struct { + ddlNode + + Undo bool + Name CIStr + Datafile string // ADD DATAFILE clause; empty when absent + UseLogfileGroup CIStr // USE LOGFILE GROUP clause; empty when absent + Options []*TablespaceOption +} + +// Restore implements Node interface. +func (n *CreateTablespaceStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.Undo { + ctx.WriteKeyWord("UNDO ") + } + ctx.WriteKeyWord("TABLESPACE ") + ctx.WriteName(n.Name.O) + if n.Datafile != "" { + ctx.WriteKeyWord(" ADD DATAFILE ") + ctx.WriteString(n.Datafile) + } + if n.UseLogfileGroup.O != "" { + ctx.WriteKeyWord(" USE LOGFILE GROUP ") + ctx.WriteName(n.UseLogfileGroup.O) + } + return restoreTablespaceOptions(ctx, n.Options, "CreateTablespaceStmt") +} + +// Accept implements Node Accept interface. +func (n *CreateTablespaceStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateTablespaceStmt) + return v.Leave(n) +} + +// AlterTablespaceOp selects the form of an ALTER TABLESPACE statement. +type AlterTablespaceOp int + +const ( + // AlterTablespaceOptionsOnly changes options alone. + AlterTablespaceOptionsOnly AlterTablespaceOp = iota + // AlterTablespaceAddDatafile is ADD DATAFILE. + AlterTablespaceAddDatafile + // AlterTablespaceDropDatafile is DROP DATAFILE. + AlterTablespaceDropDatafile + // AlterTablespaceRenameTo is RENAME TO. + AlterTablespaceRenameTo + // AlterTablespaceSetActive is SET ACTIVE. + AlterTablespaceSetActive + // AlterTablespaceSetInactive is SET INACTIVE. + AlterTablespaceSetInactive +) + +// AlterTablespaceStmt is an ALTER [UNDO] TABLESPACE statement: +// +// ALTER [UNDO] TABLESPACE tablespace_name +// {{ADD | DROP} DATAFILE 'file_name' | RENAME TO new_name +// | SET {ACTIVE | INACTIVE}} [option ...] +type AlterTablespaceStmt struct { + ddlNode + + Undo bool + Name CIStr + Op AlterTablespaceOp + Datafile string + NewName CIStr + Options []*TablespaceOption +} + +// Restore implements Node interface. +func (n *AlterTablespaceStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER ") + if n.Undo { + ctx.WriteKeyWord("UNDO ") + } + ctx.WriteKeyWord("TABLESPACE ") + ctx.WriteName(n.Name.O) + switch n.Op { + case AlterTablespaceAddDatafile: + ctx.WriteKeyWord(" ADD DATAFILE ") + ctx.WriteString(n.Datafile) + case AlterTablespaceDropDatafile: + ctx.WriteKeyWord(" DROP DATAFILE ") + ctx.WriteString(n.Datafile) + case AlterTablespaceRenameTo: + ctx.WriteKeyWord(" RENAME TO ") + ctx.WriteName(n.NewName.O) + case AlterTablespaceSetActive: + ctx.WriteKeyWord(" SET ACTIVE") + case AlterTablespaceSetInactive: + ctx.WriteKeyWord(" SET INACTIVE") + } + return restoreTablespaceOptions(ctx, n.Options, "AlterTablespaceStmt") +} + +// Accept implements Node Accept interface. +func (n *AlterTablespaceStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterTablespaceStmt) + return v.Leave(n) +} + +// DropTablespaceStmt is a DROP [UNDO] TABLESPACE statement: +// DROP [UNDO] TABLESPACE tablespace_name [ENGINE [=] engine_name]. +type DropTablespaceStmt struct { + ddlNode + + Undo bool + Name CIStr + Options []*TablespaceOption +} + +// Restore implements Node interface. +func (n *DropTablespaceStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP ") + if n.Undo { + ctx.WriteKeyWord("UNDO ") + } + ctx.WriteKeyWord("TABLESPACE ") + ctx.WriteName(n.Name.O) + return restoreTablespaceOptions(ctx, n.Options, "DropTablespaceStmt") +} + +// Accept implements Node Accept interface. +func (n *DropTablespaceStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropTablespaceStmt) + return v.Leave(n) +} + +// CreateLogfileGroupStmt is a CREATE LOGFILE GROUP statement: +// +// CREATE LOGFILE GROUP logfile_group ADD UNDOFILE 'undo_file' +// [option ...] +type CreateLogfileGroupStmt struct { + ddlNode + + GroupName CIStr + Undofile string + Options []*TablespaceOption +} + +// Restore implements Node interface. +func (n *CreateLogfileGroupStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE LOGFILE GROUP ") + ctx.WriteName(n.GroupName.O) + ctx.WriteKeyWord(" ADD UNDOFILE ") + ctx.WriteString(n.Undofile) + return restoreTablespaceOptions(ctx, n.Options, "CreateLogfileGroupStmt") +} + +// Accept implements Node Accept interface. +func (n *CreateLogfileGroupStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateLogfileGroupStmt) + return v.Leave(n) +} + +// AlterLogfileGroupStmt is an ALTER LOGFILE GROUP statement: +// +// ALTER LOGFILE GROUP logfile_group ADD UNDOFILE 'file_name' +// [option ...] +type AlterLogfileGroupStmt struct { + ddlNode + + GroupName CIStr + Undofile string + Options []*TablespaceOption +} + +// Restore implements Node interface. +func (n *AlterLogfileGroupStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER LOGFILE GROUP ") + ctx.WriteName(n.GroupName.O) + ctx.WriteKeyWord(" ADD UNDOFILE ") + ctx.WriteString(n.Undofile) + return restoreTablespaceOptions(ctx, n.Options, "AlterLogfileGroupStmt") +} + +// Accept implements Node Accept interface. +func (n *AlterLogfileGroupStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterLogfileGroupStmt) + return v.Leave(n) +} + +// DropLogfileGroupStmt is a DROP LOGFILE GROUP statement: +// DROP LOGFILE GROUP logfile_group ENGINE [=] engine_name. +type DropLogfileGroupStmt struct { + ddlNode + + GroupName CIStr + Options []*TablespaceOption +} + +// Restore implements Node interface. +func (n *DropLogfileGroupStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP LOGFILE GROUP ") + ctx.WriteName(n.GroupName.O) + return restoreTablespaceOptions(ctx, n.Options, "DropLogfileGroupStmt") +} + +// Accept implements Node Accept interface. +func (n *DropLogfileGroupStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropLogfileGroupStmt) + return v.Leave(n) +} + +/* + * Spatial reference systems + */ + +// SRSAttributeType names one attribute of CREATE SPATIAL REFERENCE +// SYSTEM. +type SRSAttributeType int + +const ( + // SRSAttributeName is NAME 'srs_name'. + SRSAttributeName SRSAttributeType = iota + // SRSAttributeDefinition is DEFINITION 'definition'. + SRSAttributeDefinition + // SRSAttributeOrganization is ORGANIZATION 'org_name' IDENTIFIED BY + // srs_id. + SRSAttributeOrganization + // SRSAttributeDescription is DESCRIPTION 'description'. + SRSAttributeDescription +) + +// String implements fmt.Stringer interface. +func (n SRSAttributeType) String() string { + switch n { + case SRSAttributeName: + return "NAME" + case SRSAttributeDefinition: + return "DEFINITION" + case SRSAttributeOrganization: + return "ORGANIZATION" + case SRSAttributeDescription: + return "DESCRIPTION" + } + return "" +} + +// SRSAttribute is one attribute of CREATE SPATIAL REFERENCE SYSTEM, +// kept in source order. OrgID belongs to SRSAttributeOrganization only. +type SRSAttribute struct { + Tp SRSAttributeType + StrValue string + OrgID uint64 +} + +// Restore implements Node interface. +func (n *SRSAttribute) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord(n.Tp.String()) + ctx.WritePlain(" ") + ctx.WriteString(n.StrValue) + if n.Tp == SRSAttributeOrganization { + ctx.WriteKeyWord(" IDENTIFIED BY ") + ctx.WritePlainf("%d", n.OrgID) + } + return nil +} + +// CreateSpatialReferenceSystemStmt is a CREATE SPATIAL REFERENCE SYSTEM +// statement: +// +// CREATE [OR REPLACE] SPATIAL REFERENCE SYSTEM [IF NOT EXISTS] srid +// srs_attribute ... +type CreateSpatialReferenceSystemStmt struct { + ddlNode + + OrReplace bool + IfNotExists bool + Srid uint64 + Attributes []*SRSAttribute +} + +// Restore implements Node interface. +func (n *CreateSpatialReferenceSystemStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.OrReplace { + ctx.WriteKeyWord("OR REPLACE ") + } + ctx.WriteKeyWord("SPATIAL REFERENCE SYSTEM ") + if n.IfNotExists { + ctx.WriteKeyWord("IF NOT EXISTS ") + } + ctx.WritePlainf("%d", n.Srid) + for i, attr := range n.Attributes { + ctx.WritePlain(" ") + if err := attr.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore CreateSpatialReferenceSystemStmt.Attributes[%d]", i) + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateSpatialReferenceSystemStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateSpatialReferenceSystemStmt) + return v.Leave(n) +} + +// DropSpatialReferenceSystemStmt is a DROP SPATIAL REFERENCE SYSTEM +// statement: DROP SPATIAL REFERENCE SYSTEM [IF EXISTS] srid. +type DropSpatialReferenceSystemStmt struct { + ddlNode + + IfExists bool + Srid uint64 +} + +// Restore implements Node interface. +func (n *DropSpatialReferenceSystemStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP SPATIAL REFERENCE SYSTEM ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + ctx.WritePlainf("%d", n.Srid) + return nil +} + +// Accept implements Node Accept interface. +func (n *DropSpatialReferenceSystemStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropSpatialReferenceSystemStmt) + return v.Leave(n) +} + +/* + * Libraries + */ + +// CreateLibraryStmt is a CREATE LIBRARY statement: +// +// CREATE [OR REPLACE] LIBRARY [IF NOT EXISTS] [schema.]library_name +// LANGUAGE language_name AS 'code' +type CreateLibraryStmt struct { + ddlNode + + OrReplace bool + IfNotExists bool + LibraryName *TableName + Language CIStr + Code string +} + +// Restore implements Node interface. +func (n *CreateLibraryStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.OrReplace { + ctx.WriteKeyWord("OR REPLACE ") + } + ctx.WriteKeyWord("LIBRARY ") + if n.IfNotExists { + ctx.WriteKeyWord("IF NOT EXISTS ") + } + if err := n.LibraryName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateLibraryStmt.LibraryName") + } + ctx.WriteKeyWord(" LANGUAGE ") + ctx.WriteKeyWord(n.Language.O) + ctx.WriteKeyWord(" AS ") + ctx.WriteString(n.Code) + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateLibraryStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateLibraryStmt) + node, ok := n.LibraryName.Accept(v) + if !ok { + return n, false + } + n.LibraryName = node.(*TableName) + return v.Leave(n) +} + +// AlterLibraryStmt is an ALTER LIBRARY statement: +// ALTER LIBRARY [schema.]library_name COMMENT 'string'. +type AlterLibraryStmt struct { + ddlNode + + LibraryName *TableName + Comment string +} + +// Restore implements Node interface. +func (n *AlterLibraryStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER LIBRARY ") + if err := n.LibraryName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterLibraryStmt.LibraryName") + } + ctx.WriteKeyWord(" COMMENT ") + ctx.WriteString(n.Comment) + return nil +} + +// Accept implements Node Accept interface. +func (n *AlterLibraryStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterLibraryStmt) + node, ok := n.LibraryName.Accept(v) + if !ok { + return n, false + } + n.LibraryName = node.(*TableName) + return v.Leave(n) +} + +// DropLibraryStmt is a DROP LIBRARY statement: +// DROP LIBRARY [IF EXISTS] [schema.]library_name. +type DropLibraryStmt struct { + ddlNode + + IfExists bool + LibraryName *TableName +} + +// Restore implements Node interface. +func (n *DropLibraryStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DROP LIBRARY ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } + if err := n.LibraryName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore DropLibraryStmt.LibraryName") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *DropLibraryStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DropLibraryStmt) + node, ok := n.LibraryName.Accept(v) + if !ok { + return n, false + } + n.LibraryName = node.(*TableName) + return v.Leave(n) +} + +/* + * JSON duality views + */ + +// CreateJSONDualityViewStmt is a CREATE JSON DUALITY VIEW statement: +// +// CREATE [OR REPLACE] JSON DUALITY VIEW [IF NOT EXISTS] view_name +// AS select_statement +type CreateJSONDualityViewStmt struct { + ddlNode + + OrReplace bool + IfNotExists bool + ViewName *TableName + Select StmtNode +} + +// Restore implements Node interface. +func (n *CreateJSONDualityViewStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("CREATE ") + if n.OrReplace { + ctx.WriteKeyWord("OR REPLACE ") + } + ctx.WriteKeyWord("JSON DUALITY VIEW ") + if n.IfNotExists { + ctx.WriteKeyWord("IF NOT EXISTS ") + } + if err := n.ViewName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateJSONDualityViewStmt.ViewName") + } + ctx.WriteKeyWord(" AS ") + if err := n.Select.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateJSONDualityViewStmt.Select") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *CreateJSONDualityViewStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*CreateJSONDualityViewStmt) + node, ok := n.ViewName.Accept(v) + if !ok { + return n, false + } + n.ViewName = node.(*TableName) + selnode, ok := n.Select.Accept(v) + if !ok { + return n, false + } + n.Select = selnode.(StmtNode) + return v.Leave(n) +} + +// AlterJSONDualityViewStmt is an ALTER JSON DUALITY VIEW statement: +// ALTER JSON DUALITY VIEW view_name AS select_statement. +type AlterJSONDualityViewStmt struct { + ddlNode + + ViewName *TableName + Select StmtNode +} + +// Restore implements Node interface. +func (n *AlterJSONDualityViewStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("ALTER JSON DUALITY VIEW ") + if err := n.ViewName.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterJSONDualityViewStmt.ViewName") + } + ctx.WriteKeyWord(" AS ") + if err := n.Select.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterJSONDualityViewStmt.Select") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *AlterJSONDualityViewStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*AlterJSONDualityViewStmt) + node, ok := n.ViewName.Accept(v) + if !ok { + return n, false + } + n.ViewName = node.(*TableName) + selnode, ok := n.Select.Accept(v) + if !ok { + return n, false + } + n.Select = selnode.(StmtNode) + return v.Leave(n) +} + +// JSONDualityObjectItem is one 'key' : value pair of a +// JSON_DUALITY_OBJECT expression. +type JSONDualityObjectItem struct { + Key string + Value ExprNode +} + +// JSONDualityObjectExpr is a JSON_DUALITY_OBJECT('key' : value, ...) +// expression, the select-list constructor of JSON duality views. +type JSONDualityObjectExpr struct { + exprNode + + Items []*JSONDualityObjectItem +} + +// Restore implements Node interface. +func (n *JSONDualityObjectExpr) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("JSON_DUALITY_OBJECT") + ctx.WritePlain("(") + for i, item := range n.Items { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WriteString(item.Key) + ctx.WritePlain(" : ") + if err := item.Value.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore JSONDualityObjectExpr.Items[%d]", i) + } + } + ctx.WritePlain(")") + return nil +} + +// Format the ExprNode into a Writer. +func (n *JSONDualityObjectExpr) Format(w io.Writer) { + panic("Not implemented") +} + +// Accept implements Node Accept interface. +func (n *JSONDualityObjectExpr) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*JSONDualityObjectExpr) + for _, item := range n.Items { + node, ok := item.Value.Accept(v) + if !ok { + return n, false + } + item.Value = node.(ExprNode) + } + return v.Leave(n) +} diff --git a/ast/procedure.go b/ast/procedure.go index 698e082..6084950 100644 --- a/ast/procedure.go +++ b/ast/procedure.go @@ -17,6 +17,7 @@ import ( "fmt" "strconv" + "github.com/sqlc-dev/marino/auth" "github.com/sqlc-dev/marino/format" "github.com/sqlc-dev/marino/types" ) @@ -238,11 +239,22 @@ type ProcedureInfo struct { ProcedureParam []*StoreParameter //procedure param ProcedureBody StmtNode //procedure body statement ProcedureParamStr string //procedure parameter string + // Definer is the DEFINER = user clause; nil when absent. + Definer *auth.UserIdentity } // Restore implements Node interface. func (n *ProcedureInfo) Restore(ctx *format.RestoreCtx) error { - ctx.WriteKeyWord("CREATE PROCEDURE ") + ctx.WriteKeyWord("CREATE ") + if n.Definer != nil { + ctx.WriteKeyWord("DEFINER ") + ctx.WritePlain("= ") + if err := n.Definer.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ProcedureInfo.Definer") + } + ctx.WritePlain(" ") + } + ctx.WriteKeyWord("PROCEDURE ") if n.IfNotExists { ctx.WriteKeyWord("IF NOT EXISTS ") } diff --git a/ast/sem.go b/ast/sem.go index abb87ba..4152927 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -568,6 +568,62 @@ const ( ImportTableCommand = "IMPORT TABLE" // LoadXMLCommand represents LOAD XML statement LoadXMLCommand = "LOAD XML" + // CreateEventCommand represents CREATE EVENT statement + CreateEventCommand = "CREATE EVENT" + // AlterEventCommand represents ALTER EVENT statement + AlterEventCommand = "ALTER EVENT" + // DropEventCommand represents DROP EVENT statement + DropEventCommand = "DROP EVENT" + // CreateTriggerCommand represents CREATE TRIGGER statement + CreateTriggerCommand = "CREATE TRIGGER" + // DropTriggerCommand represents DROP TRIGGER statement + DropTriggerCommand = "DROP TRIGGER" + // CreateFunctionCommand represents CREATE FUNCTION statement for stored functions + CreateFunctionCommand = "CREATE FUNCTION" + // AlterFunctionCommand represents ALTER FUNCTION statement + AlterFunctionCommand = "ALTER FUNCTION" + // DropFunctionCommand represents DROP FUNCTION statement + DropFunctionCommand = "DROP FUNCTION" + // AlterProcedureCommand represents ALTER PROCEDURE statement + AlterProcedureCommand = "ALTER PROCEDURE" + // ReturnCommand represents RETURN statement + ReturnCommand = "RETURN" + // AlterViewCommand represents ALTER VIEW statement + AlterViewCommand = "ALTER VIEW" + // CreateServerCommand represents CREATE SERVER statement + CreateServerCommand = "CREATE SERVER" + // AlterServerCommand represents ALTER SERVER statement + AlterServerCommand = "ALTER SERVER" + // DropServerCommand represents DROP SERVER statement + DropServerCommand = "DROP SERVER" + // CreateTablespaceCommand represents CREATE TABLESPACE statement + CreateTablespaceCommand = "CREATE TABLESPACE" + // AlterTablespaceCommand represents ALTER TABLESPACE statement + AlterTablespaceCommand = "ALTER TABLESPACE" + // DropTablespaceCommand represents DROP TABLESPACE statement + DropTablespaceCommand = "DROP TABLESPACE" + // CreateLogfileGroupCommand represents CREATE LOGFILE GROUP statement + CreateLogfileGroupCommand = "CREATE LOGFILE GROUP" + // AlterLogfileGroupCommand represents ALTER LOGFILE GROUP statement + AlterLogfileGroupCommand = "ALTER LOGFILE GROUP" + // DropLogfileGroupCommand represents DROP LOGFILE GROUP statement + DropLogfileGroupCommand = "DROP LOGFILE GROUP" + // CreateSpatialReferenceSystemCommand represents CREATE SPATIAL REFERENCE SYSTEM statement + CreateSpatialReferenceSystemCommand = "CREATE SPATIAL REFERENCE SYSTEM" + // DropSpatialReferenceSystemCommand represents DROP SPATIAL REFERENCE SYSTEM statement + DropSpatialReferenceSystemCommand = "DROP SPATIAL REFERENCE SYSTEM" + // CreateLibraryCommand represents CREATE LIBRARY statement + CreateLibraryCommand = "CREATE LIBRARY" + // AlterLibraryCommand represents ALTER LIBRARY statement + AlterLibraryCommand = "ALTER LIBRARY" + // DropLibraryCommand represents DROP LIBRARY statement + DropLibraryCommand = "DROP LIBRARY" + // CreateJSONDualityViewCommand represents CREATE JSON DUALITY VIEW statement + CreateJSONDualityViewCommand = "CREATE JSON DUALITY VIEW" + // AlterJSONDualityViewCommand represents ALTER JSON DUALITY VIEW statement + AlterJSONDualityViewCommand = "ALTER JSON DUALITY VIEW" + // DropMaskingPolicyCommand represents DROP MASKING POLICY statement + DropMaskingPolicyCommand = "DROP MASKING POLICY" // UnknownCommand represents unknown statements UnknownCommand = "UNKNOWN" // SetOprCommand represents UNION/INTERSECT/EXCEPT statement @@ -1638,3 +1694,143 @@ func (n *ImportTableStmt) SEMCommand() string { func (n *LoadXMLStmt) SEMCommand() string { return LoadXMLCommand } + +// SEMCommand returns the command string for the statement. +func (n *CreateEventStmt) SEMCommand() string { + return CreateEventCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterEventStmt) SEMCommand() string { + return AlterEventCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropEventStmt) SEMCommand() string { + return DropEventCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateTriggerStmt) SEMCommand() string { + return CreateTriggerCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropTriggerStmt) SEMCommand() string { + return DropTriggerCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateFunctionStmt) SEMCommand() string { + return CreateFunctionCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterFunctionStmt) SEMCommand() string { + return AlterFunctionCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropFunctionStmt) SEMCommand() string { + return DropFunctionCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterProcedureStmt) SEMCommand() string { + return AlterProcedureCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ReturnStmt) SEMCommand() string { + return ReturnCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterViewStmt) SEMCommand() string { + return AlterViewCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateServerStmt) SEMCommand() string { + return CreateServerCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterServerStmt) SEMCommand() string { + return AlterServerCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropServerStmt) SEMCommand() string { + return DropServerCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateTablespaceStmt) SEMCommand() string { + return CreateTablespaceCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterTablespaceStmt) SEMCommand() string { + return AlterTablespaceCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropTablespaceStmt) SEMCommand() string { + return DropTablespaceCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateLogfileGroupStmt) SEMCommand() string { + return CreateLogfileGroupCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterLogfileGroupStmt) SEMCommand() string { + return AlterLogfileGroupCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropLogfileGroupStmt) SEMCommand() string { + return DropLogfileGroupCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateSpatialReferenceSystemStmt) SEMCommand() string { + return CreateSpatialReferenceSystemCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropSpatialReferenceSystemStmt) SEMCommand() string { + return DropSpatialReferenceSystemCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateLibraryStmt) SEMCommand() string { + return CreateLibraryCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterLibraryStmt) SEMCommand() string { + return AlterLibraryCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropLibraryStmt) SEMCommand() string { + return DropLibraryCommand +} + +// SEMCommand returns the command string for the statement. +func (n *CreateJSONDualityViewStmt) SEMCommand() string { + return CreateJSONDualityViewCommand +} + +// SEMCommand returns the command string for the statement. +func (n *AlterJSONDualityViewStmt) SEMCommand() string { + return AlterJSONDualityViewCommand +} + +// SEMCommand returns the command string for the statement. +func (n *DropMaskingPolicyStmt) SEMCommand() string { + return DropMaskingPolicyCommand +} diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index e8b5d7a..9e3e08f 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -445,6 +445,23 @@ var unReservedKeywordNames = []string{ "DUMPFILE", "PREV", "XML", + "AT", + "COMPLETION", + "CONTAINS", + "DATAFILE", + "DUALITY", + "ENDS", + "EVERY", + "FOLLOWS", + "INNODB", + "LOGFILE", + "OPTIONS", + "PRECEDES", + "ROTATE", + "SERVER", + "STARTS", + "UNDOFILE", + "WRAPPER", "CODE", "LIBRARY", "MUTEX", @@ -454,6 +471,7 @@ var unReservedKeywordNames = []string{ // notKeywordTokenNames lists the NotKeywordToken production alternatives of parser.y. var notKeywordTokenNames = []string{ + "JSON_DUALITY_OBJECT", "ADDDATE", "APPROX_COUNT_DISTINCT", "APPROX_PERCENTILE", diff --git a/parser/keywords.go b/parser/keywords.go index 4605fb2..25f3b14 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -76,12 +76,14 @@ var Keywords = []KeywordsType{ {"DENSE_RANK", true, "reserved"}, {"DESC", true, "reserved"}, {"DESCRIBE", true, "reserved"}, + {"DETERMINISTIC", true, "reserved"}, {"DISTINCT", true, "reserved"}, {"DISTINCTROW", true, "reserved"}, {"DIV", true, "reserved"}, {"DOUBLE", true, "reserved"}, {"DROP", true, "reserved"}, {"DUAL", true, "reserved"}, + {"EACH", true, "reserved"}, {"ELSE", true, "reserved"}, {"ELSEIF", true, "reserved"}, {"ENCLOSED", true, "reserved"}, @@ -165,6 +167,7 @@ var Keywords = []KeywordsType{ {"MINUTE_MICROSECOND", true, "reserved"}, {"MINUTE_SECOND", true, "reserved"}, {"MOD", true, "reserved"}, + {"MODIFIES", true, "reserved"}, {"NATURAL", true, "reserved"}, {"NOT", true, "reserved"}, {"NO_WRITE_TO_BINLOG", true, "reserved"}, @@ -192,6 +195,7 @@ var Keywords = []KeywordsType{ {"RANGE", true, "reserved"}, {"RANK", true, "reserved"}, {"READ", true, "reserved"}, + {"READS", true, "reserved"}, {"REAL", true, "reserved"}, {"RECURSIVE", true, "reserved"}, {"REFERENCES", true, "reserved"}, @@ -203,6 +207,7 @@ var Keywords = []KeywordsType{ {"REQUIRE", true, "reserved"}, {"RESIGNAL", true, "reserved"}, {"RESTRICT", true, "reserved"}, + {"RETURN", true, "reserved"}, {"REVOKE", true, "reserved"}, {"RIGHT", true, "reserved"}, {"RLIKE", true, "reserved"}, @@ -239,6 +244,7 @@ var Keywords = []KeywordsType{ {"TRAILING", true, "reserved"}, {"TRIGGER", true, "reserved"}, {"TRUE", true, "reserved"}, + {"UNDO", true, "reserved"}, {"UNION", true, "reserved"}, {"UNIQUE", true, "reserved"}, {"UNLOCK", true, "reserved"}, @@ -280,6 +286,7 @@ var Keywords = []KeywordsType{ {"ANY", false, "unreserved"}, {"APPLY", false, "unreserved"}, {"ASCII", false, "unreserved"}, + {"AT", false, "unreserved"}, {"ATTRIBUTE", false, "unreserved"}, {"ATTRIBUTES", false, "unreserved"}, {"AUTOEXTEND_SIZE", false, "unreserved"}, @@ -335,6 +342,7 @@ var Keywords = []KeywordsType{ {"COMMIT", false, "unreserved"}, {"COMMITTED", false, "unreserved"}, {"COMPACT", false, "unreserved"}, + {"COMPLETION", false, "unreserved"}, {"COMPONENT", false, "unreserved"}, {"COMPRESSED", false, "unreserved"}, {"COMPRESSION", false, "unreserved"}, @@ -346,6 +354,7 @@ var Keywords = []KeywordsType{ {"CONNECTION", false, "unreserved"}, {"CONSISTENCY", false, "unreserved"}, {"CONSISTENT", false, "unreserved"}, + {"CONTAINS", false, "unreserved"}, {"CONTEXT", false, "unreserved"}, {"CPU", false, "unreserved"}, {"CSV_BACKSLASH_ESCAPE", false, "unreserved"}, @@ -358,6 +367,7 @@ var Keywords = []KeywordsType{ {"CURRENT", false, "unreserved"}, {"CYCLE", false, "unreserved"}, {"DATA", false, "unreserved"}, + {"DATAFILE", false, "unreserved"}, {"DATE", false, "unreserved"}, {"DATETIME", false, "unreserved"}, {"DAY", false, "unreserved"}, @@ -373,6 +383,7 @@ var Keywords = []KeywordsType{ {"DISCARD", false, "unreserved"}, {"DISK", false, "unreserved"}, {"DO", false, "unreserved"}, + {"DUALITY", false, "unreserved"}, {"DUMPFILE", false, "unreserved"}, {"DUPLICATE", false, "unreserved"}, {"DYNAMIC", false, "unreserved"}, @@ -382,6 +393,7 @@ var Keywords = []KeywordsType{ {"ENCRYPTION_KEYFILE", false, "unreserved"}, {"ENCRYPTION_METHOD", false, "unreserved"}, {"END", false, "unreserved"}, + {"ENDS", false, "unreserved"}, {"ENFORCED", false, "unreserved"}, {"ENGINE", false, "unreserved"}, {"ENGINES", false, "unreserved"}, @@ -392,6 +404,7 @@ var Keywords = []KeywordsType{ {"ESCAPE", false, "unreserved"}, {"EVENT", false, "unreserved"}, {"EVENTS", false, "unreserved"}, + {"EVERY", false, "unreserved"}, {"EVOLVE", false, "unreserved"}, {"EXCHANGE", false, "unreserved"}, {"EXCLUSIVE", false, "unreserved"}, @@ -410,6 +423,7 @@ var Keywords = []KeywordsType{ {"FIXED", false, "unreserved"}, {"FLUSH", false, "unreserved"}, {"FOLLOWING", false, "unreserved"}, + {"FOLLOWS", false, "unreserved"}, {"FORMAT", false, "unreserved"}, {"FOUND", false, "unreserved"}, {"FULL", false, "unreserved"}, @@ -435,6 +449,7 @@ var Keywords = []KeywordsType{ {"INCREMENT", false, "unreserved"}, {"INCREMENTAL", false, "unreserved"}, {"INDEXES", false, "unreserved"}, + {"INNODB", false, "unreserved"}, {"INSERT_METHOD", false, "unreserved"}, {"INSTALL", false, "unreserved"}, {"INSTANCE", false, "unreserved"}, @@ -461,6 +476,7 @@ var Keywords = []KeywordsType{ {"LOCAL", false, "unreserved"}, {"LOCATION", false, "unreserved"}, {"LOCKED", false, "unreserved"}, + {"LOGFILE", false, "unreserved"}, {"LOGS", false, "unreserved"}, {"MANUAL", false, "unreserved"}, {"MASKING", false, "unreserved"}, @@ -513,6 +529,7 @@ var Keywords = []KeywordsType{ {"ON_DUPLICATE", false, "unreserved"}, {"OPEN", false, "unreserved"}, {"OPTIONAL", false, "unreserved"}, + {"OPTIONS", false, "unreserved"}, {"PACK_KEYS", false, "unreserved"}, {"PAGE", false, "unreserved"}, {"PAGE_CHECKSUM", false, "unreserved"}, @@ -536,6 +553,7 @@ var Keywords = []KeywordsType{ {"PLUGINS", false, "unreserved"}, {"POINT", false, "unreserved"}, {"POLICY", false, "unreserved"}, + {"PRECEDES", false, "unreserved"}, {"PRECEDING", false, "unreserved"}, {"PREPARE", false, "unreserved"}, {"PRESERVE", false, "unreserved"}, @@ -580,6 +598,7 @@ var Keywords = []KeywordsType{ {"ROLE", false, "unreserved"}, {"ROLLBACK", false, "unreserved"}, {"ROLLUP", false, "unreserved"}, + {"ROTATE", false, "unreserved"}, {"ROUTINE", false, "unreserved"}, {"ROW_COUNT", false, "unreserved"}, {"ROW_FORMAT", false, "unreserved"}, @@ -599,6 +618,7 @@ var Keywords = []KeywordsType{ {"SEQUENCE", false, "unreserved"}, {"SERIAL", false, "unreserved"}, {"SERIALIZABLE", false, "unreserved"}, + {"SERVER", false, "unreserved"}, {"SESSION", false, "unreserved"}, {"SETVAL", false, "unreserved"}, {"SHARD_ROW_ID_BITS", false, "unreserved"}, @@ -629,6 +649,7 @@ var Keywords = []KeywordsType{ {"SQL_TSI_YEAR", false, "unreserved"}, {"STACKED", false, "unreserved"}, {"START", false, "unreserved"}, + {"STARTS", false, "unreserved"}, {"STATS_AUTO_RECALC", false, "unreserved"}, {"STATS_COL_CHOICE", false, "unreserved"}, {"STATS_COL_LIST", false, "unreserved"}, @@ -678,6 +699,7 @@ var Keywords = []KeywordsType{ {"UNBOUNDED", false, "unreserved"}, {"UNCOMMITTED", false, "unreserved"}, {"UNDEFINED", false, "unreserved"}, + {"UNDOFILE", false, "unreserved"}, {"UNICODE", false, "unreserved"}, {"UNINSTALL", false, "unreserved"}, {"UNKNOWN", false, "unreserved"}, @@ -700,6 +722,7 @@ var Keywords = []KeywordsType{ {"WITHOUT", false, "unreserved"}, {"WITH_SYS_TABLE", false, "unreserved"}, {"WORKLOAD", false, "unreserved"}, + {"WRAPPER", false, "unreserved"}, {"X509", false, "unreserved"}, {"XA", false, "unreserved"}, {"XID", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 1a60048..a5f4b4f 100644 --- a/parser/keywords_test.go +++ b/parser/keywords_test.go @@ -43,8 +43,8 @@ func TestKeywords(t *testing.T) { } func TestKeywordsLength(t *testing.T) { - if !reflect.DeepEqual(728, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 728) + if !reflect.DeepEqual(751, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 751) } reservedNr := 0 @@ -53,8 +53,8 @@ func TestKeywordsLength(t *testing.T) { reservedNr += 1 } } - if !reflect.DeepEqual(240, reservedNr) { - t.Fatalf("got %v, want %v", reservedNr, 240) + if !reflect.DeepEqual(246, reservedNr) { + t.Fatalf("got %v, want %v", reservedNr, 246) } } diff --git a/parser/lexer_test.go b/parser/lexer_test.go index 7086b6f..b7f1a4a 100644 --- a/parser/lexer_test.go +++ b/parser/lexer_test.go @@ -58,7 +58,7 @@ type testLiteralValue struct { func TestSingleCharOther(t *testing.T) { table := []testCaseItem{ - {"AT", identifier}, + {"AT", at}, {"?", paramMarker}, {"PLACEHOLDER", identifier}, {"=", eq}, diff --git a/parser/misc.go b/parser/misc.go index 1d7be26..d01329a 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -182,6 +182,7 @@ var tokenMap = map[string]int{ "ASC": asc, "ASCII": ascii, "APPLY": apply, + "AT": at, "ATTRIBUTE": attribute, "ATTRIBUTES": attributes, "BATCH": batch, @@ -272,6 +273,7 @@ var tokenMap = map[string]int{ "COMMIT": commit, "COMMITTED": committed, "COMPACT": compact, + "COMPLETION": completion, "COMPONENT": component, "COMPRESS": compress, "COMPRESSED": compressed, @@ -284,6 +286,7 @@ var tokenMap = map[string]int{ "CONSISTENT": consistent, "CONSTRAINT": constraint, "CONSTRAINTS": constraints, + "CONTAINS": contains, "CONTEXT": context, "CONTINUE": continueKwd, "CONVERT": convert, @@ -325,6 +328,7 @@ var tokenMap = map[string]int{ "DATABASES": databases, "DATE_ADD": dateAdd, "DATE_SUB": dateSub, + "DATAFILE": datafile, "DATE": dateType, "DATETIME": datetimeType, "DAY_HOUR": dayHour, @@ -347,6 +351,7 @@ var tokenMap = map[string]int{ "DEPTH": depth, "DESC": desc, "DESCRIBE": describe, + "DETERMINISTIC": deterministic, "DIAGNOSTICS": diagnostics, "DIGEST": digest, "DIRECTORY": directory, @@ -367,11 +372,13 @@ var tokenMap = map[string]int{ "DRY": dry, "DRYRUN": dryRun, "DUAL": dual, + "DUALITY": duality, "DUMP": dump, "DUMPFILE": dumpfile, "DUPLICATE": duplicate, "DURATION": timeDuration, "DYNAMIC": dynamic, + "EACH": each, "ELSE": elseKwd, "ELSEIF": elseIfKwd, "ENABLE": enable, @@ -379,6 +386,7 @@ var tokenMap = map[string]int{ "ENCLOSED": enclosed, "ENCRYPTION": encryption, "END": end, + "ENDS": ends, "END_TIME": endTime, "ENFORCED": enforced, "ENGINE": engine, @@ -392,6 +400,7 @@ var tokenMap = map[string]int{ "ESCAPED": escaped, "EVENT": event, "EVENTS": events, + "EVERY": every, "EVOLVE": evolve, "EXACT": exact, "EXEC_ELAPSED": execElapsed, @@ -427,6 +436,7 @@ var tokenMap = map[string]int{ "FOLLOWERS": followers, "FOLLOWER_CONSTRAINTS": followerConstraints, "FOLLOWING": following, + "FOLLOWS": follows, "FOR": forKwd, "FORCE": force, "FOREIGN": foreign, @@ -476,6 +486,7 @@ var tokenMap = map[string]int{ "INDEXES": indexes, "INFILE": infile, "INNER": inner, + "INNODB": innodb, "INOUT": inout, "INPLACE": inplace, "INSERT_METHOD": insertMethod, @@ -517,6 +528,7 @@ var tokenMap = map[string]int{ "JSON_ARRAYAGG": jsonArrayagg, "JSON_OBJECTAGG": jsonObjectAgg, "JSON_SUM_CRC32": jsonSumCrc32, + "JSON_DUALITY_OBJECT": jsonDualityObject, "JSON": jsonType, "KEY_BLOCK_SIZE": keyBlockSize, "KEY": key, @@ -554,6 +566,7 @@ var tokenMap = map[string]int{ "LOCK": lock, "LOCKED": locked, "LOG": log, + "LOGFILE": logfile, "LOGS": logs, "LONG": long, "LONGBLOB": longblobType, @@ -591,6 +604,7 @@ var tokenMap = map[string]int{ "MINVALUE": minValue, "MOD": mod, "MODE": mode, + "MODIFIES": modifies, "MODIFY": modify, "MONITOR": monitor, "MONTH": month, @@ -640,6 +654,7 @@ var tokenMap = map[string]int{ "OPTION": option, "OPTIONAL": optional, "OPTIONALLY": optionally, + "OPTIONS": options, "OR": or, "ORDER": order, "OUT": out, @@ -675,6 +690,7 @@ var tokenMap = map[string]int{ "POLICY": policy, "POSITION": position, "PRE_SPLIT_REGIONS": preSplitRegions, + "PRECEDES": precedes, "PRECEDING": preceding, "PREDICATE": predicate, "PRECISION": precisionType, @@ -702,6 +718,7 @@ var tokenMap = map[string]int{ "RATE_LIMIT": rateLimit, "RAW": raw, "READ": read, + "READS": reads, "READ_ONLY": readOnly, "REAL": realType, "REBUILD": rebuild, @@ -750,6 +767,7 @@ var tokenMap = map[string]int{ "ROLE": role, "ROLLBACK": rollback, "ROLLUP": rollup, + "ROTATE": rotate, "ROUTINE": routine, "ROW_COUNT": rowCount, "ROW_FORMAT": rowFormat, @@ -758,6 +776,7 @@ var tokenMap = map[string]int{ "RTREE": rtree, "HYPO": hypo, "RESUME": resume, + "RETURN": returnKwd, "RETURNS": returns, "RUN": run, "RUNNING": running, @@ -783,6 +802,7 @@ var tokenMap = map[string]int{ "SEQUENCE": sequence, "SERIAL": serial, "SERIALIZABLE": serializable, + "SERVER": server, "SESSION": session, "SESSION_STATES": sessionStates, "SET": set, @@ -834,6 +854,7 @@ var tokenMap = map[string]int{ "START_TIME": startTime, "START_TS": startTS, "STARTING": starting, + "STARTS": starts, "STATISTICS": statistics, "STATS_AUTO_RECALC": statsAutoRecalc, "STATS_BUCKETS": statsBuckets, @@ -938,6 +959,8 @@ var tokenMap = map[string]int{ "UNBOUNDED": unbounded, "UNCOMMITTED": uncommitted, "UNDEFINED": undefined, + "UNDO": undo, + "UNDOFILE": undofile, "UNICODE": unicodeSym, "UNINSTALL": uninstall, "UNION": union, @@ -991,6 +1014,7 @@ var tokenMap = map[string]int{ "WIDTH": width, "WITH": with, "WITHOUT": without, + "WRAPPER": wrapper, "WRITE": write, "WORKLOAD": workload, "X509": x509, diff --git a/parser/parse_alter.go b/parser/parse_alter.go index bdfc9c3..70f175a 100644 --- a/parser/parse_alter.go +++ b/parser/parse_alter.go @@ -57,6 +57,31 @@ func (r *rdParser) parseAlterStmtFamily() ast.StmtNode { return r.parseAlterPolicyStmt() case resource: return r.parseAlterResourceGroupStmt() + case event: + return r.parseAlterEventStmt() + case view, algorithm, sql: + return r.parseAlterViewStmt() + case definer: + // "ALTER" "DEFINER" eq Username prefixes views and events; the + // token after the clause decides. + if r.peekPastDefiner() == event { + return r.parseAlterEventStmt() + } + return r.parseAlterViewStmt() + case procedure: + return r.parseAlterProcedureStmt() + case function: + return r.parseAlterFunctionStmt() + case server: + return r.parseAlterServerStmt() + case tablespace, undo: + return r.parseAlterTablespaceStmt() + case logfile: + return r.parseAlterLogfileGroupStmt() + case library: + return r.parseAlterLibraryStmt() + case jsonType: + return r.parseAlterJSONDualityViewStmt() default: r.unsupported(fmt.Sprintf("ALTER %s", r.at(r.i+1).lit)) return nil @@ -1498,25 +1523,62 @@ func (r *rdParser) parseAlterUserStmt() ast.StmtNode { /**************************************Other ALTER kinds***************************************/ -// parseAlterInstanceStmt implements AlterInstanceStmt and InstanceOption. +// parseAlterInstanceStmt implements AlterInstanceStmt and +// InstanceOption: +// +// "ROTATE" ("INNODB" | "BINLOG") "MASTER" "KEY" +// | "RELOAD" "TLS" ["FOR" "CHANNEL" Identifier] +// ["NO" "ROLLBACK" "ON" "ERROR"] +// | "RELOAD" "KEYRING" +// | ("ENABLE" | "DISABLE") "INNODB" "REDO_LOG" +// +// KEYRING and REDO_LOG are not keywords and are matched in identifier +// position. func (r *rdParser) parseAlterInstanceStmt() ast.StmtNode { r.expect(alter) r.expect(instance) - // InstanceOption: "RELOAD" "TLS" ["NO" "ROLLBACK" "ON" "ERROR"] + switch r.tok() { + case rotate: + r.advance() + stmt := &ast.AlterInstanceStmt{} + switch r.tok() { + case innodb: + stmt.RotateInnoDBMasterKey = true + case binlog: + stmt.RotateBinlogMasterKey = true + default: + r.syntaxError() + } + r.advance() + r.expect(master) + r.expect(key) + return stmt + case enable, disable: + stmt := &ast.AlterInstanceStmt{EnableInnoDBRedoLog: r.tok() == enable} + stmt.DisableInnoDBRedoLog = !stmt.EnableInnoDBRedoLog + r.advance() + r.expect(innodb) + r.expectIdentLit("REDO_LOG") + return stmt + } r.expect(reload) - r.expect(tls) + if r.tok() != tls { + r.expectIdentLit("KEYRING") + return &ast.AlterInstanceStmt{ReloadKeyring: true} + } + r.advance() + stmt := &ast.AlterInstanceStmt{ReloadTLS: true} + if r.accept(forKwd) { + r.expect(channel) + stmt.Channel = r.parseIdentifier() + } if r.accept(no) { r.expect(rollback) r.expect(on) r.expect(errorKwd) - return &ast.AlterInstanceStmt{ - ReloadTLS: true, - NoRollbackOnError: true, - } - } - return &ast.AlterInstanceStmt{ - ReloadTLS: true, + stmt.NoRollbackOnError = true } + return stmt } // parseAlterRangeStmt implements AlterRangeStmt: diff --git a/parser/parse_create_misc.go b/parser/parse_create_misc.go index 69bc145..a9b5bda 100644 --- a/parser/parse_create_misc.go +++ b/parser/parse_create_misc.go @@ -995,8 +995,9 @@ func (r *rdParser) parseDirectResourceGroupBackgroundOption() *ast.ResourceGroup // parseCreateMaskingPolicyStmt implements: // // CreateMaskingPolicyStmt: "CREATE" OrReplace "MASKING" "POLICY" -// IfNotExists PolicyName "ON" TableName '(' Identifier ')' "AS" -// Expression MaskingPolicyRestrictOnOpt MaskingPolicyStateOpt +// IfNotExists PolicyName "ON" TableName '(' Identifier ')' +// ("AS" Expression | "USING" '(' Expression ')') +// MaskingPolicyRestrictOnOpt MaskingPolicyStateOpt func (r *rdParser) parseCreateMaskingPolicyStmt() ast.StmtNode { r.expect(create) orReplace := false @@ -1014,8 +1015,17 @@ func (r *rdParser) parseCreateMaskingPolicyStmt() ast.StmtNode { r.expect(int('(')) column := r.parseIdentifier() r.expect(int(')')) - r.expect(as) - expr := r.parseExpression() + var expr ast.ExprNode + usingForm := false + if r.accept(using) { + usingForm = true + r.expect(int('(')) + expr = r.parseExpression() + r.expect(int(')')) + } else { + r.expect(as) + expr = r.parseExpression() + } restrictOps := r.parseMaskingPolicyRestrictOnOpt() state := r.parseMaskingPolicyStateOpt() if orReplace && ifNotExists { @@ -1030,6 +1040,7 @@ func (r *rdParser) parseCreateMaskingPolicyStmt() ast.StmtNode { Expr: expr, RestrictOps: restrictOps, MaskingPolicyState: *state, + Using: usingForm, } } diff --git a/parser/parse_create_table.go b/parser/parse_create_table.go index 3b8364a..c01122a 100644 --- a/parser/parse_create_table.go +++ b/parser/parse_create_table.go @@ -49,12 +49,34 @@ func (r *rdParser) parseCreateStmtFamily() ast.StmtNode { return r.parseCreateBindingStmt() case database: return r.parseCreateDatabaseStmt() - case index, unique, spatial, fulltext, vectorType, columnar: + case spatial: + if r.la(2) != index { + // "CREATE" "SPATIAL" "REFERENCE" "SYSTEM" ... + return r.parseCreateSpatialReferenceSystemStmt() + } + return r.parseCreateIndexStmt() + case index, unique, fulltext, vectorType, columnar: // IndexKeyTypeOpt "INDEX" return r.parseCreateIndexStmt() - case view, algorithm, definer, sql: - // ViewAlgorithm/ViewDefiner/ViewSQLSecurity ... "VIEW" + case view, algorithm, sql: + // ViewAlgorithm/ViewSQLSecurity ... "VIEW" return r.parseCreateViewStmt() + case definer: + // "CREATE" "DEFINER" eq Username prefixes views, events, + // triggers, and stored routines; the token after the clause + // decides. + switch r.peekPastDefiner() { + case event: + return r.parseCreateEventStmt() + case trigger: + return r.parseCreateTriggerStmt() + case procedure: + return r.parseCreateProcedureStmt() + case function: + return r.parseCreateFunctionFamily() + default: + return r.parseCreateViewStmt() + } case or: // "CREATE" "OR" "REPLACE" ... switch r.la(3) { @@ -62,6 +84,12 @@ func (r *rdParser) parseCreateStmtFamily() ast.StmtNode { return r.parseCreatePolicyStmt() case masking: return r.parseCreateMaskingPolicyStmt() + case spatial: + return r.parseCreateSpatialReferenceSystemStmt() + case library: + return r.parseCreateLibraryStmt() + case jsonType: + return r.parseCreateJSONDualityViewStmt() default: return r.parseCreateViewStmt() } @@ -85,9 +113,21 @@ func (r *rdParser) parseCreateStmtFamily() ast.StmtNode { case procedure: return r.parseCreateProcedureStmt() case function, aggregate: - // Only the loadable function form parses; a stored function - // fails inside (at its parameter list). - return r.parseCreateLoadableFunctionStmt() + return r.parseCreateFunctionFamily() + case event: + return r.parseCreateEventStmt() + case trigger: + return r.parseCreateTriggerStmt() + case server: + return r.parseCreateServerStmt() + case tablespace, undo: + return r.parseCreateTablespaceStmt() + case logfile: + return r.parseCreateLogfileGroupStmt() + case library: + return r.parseCreateLibraryStmt() + case jsonType: + return r.parseCreateJSONDualityViewStmt() default: // No production continues here; the automaton shifts CREATE and // errors at the lookahead, so advance before reporting. diff --git a/parser/parse_drop.go b/parser/parse_drop.go index ca23953..724486e 100644 --- a/parser/parse_drop.go +++ b/parser/parse_drop.go @@ -95,6 +95,77 @@ func (r *rdParser) parseDropStmtFamily() ast.StmtNode { return r.parseDropBindingStmt() case procedure: return r.parseDropProcedureStmt() + case event: + // DropEventStmt: "DROP" "EVENT" IfExists TableName + r.advance() + r.advance() + ifExists := r.parseIfExists() + return &ast.DropEventStmt{IfExists: ifExists, EventName: r.parseTableName()} + case trigger: + // DropTriggerStmt: "DROP" "TRIGGER" IfExists TableName + r.advance() + r.advance() + ifExists := r.parseIfExists() + return &ast.DropTriggerStmt{IfExists: ifExists, TriggerName: r.parseTableName()} + case function: + // DropFunctionStmt: "DROP" "FUNCTION" IfExists TableName + r.advance() + r.advance() + ifExists := r.parseIfExists() + return &ast.DropFunctionStmt{IfExists: ifExists, FunctionName: r.parseTableName()} + case server: + // DropServerStmt: "DROP" "SERVER" IfExists Identifier + r.advance() + r.advance() + ifExists := r.parseIfExists() + return &ast.DropServerStmt{IfExists: ifExists, ServerName: ast.NewCIStr(r.parseIdentifier())} + case tablespace, undo: + // DropTablespaceStmt: "DROP" ["UNDO"] "TABLESPACE" Identifier + // TablespaceOptions + r.advance() + stmt := &ast.DropTablespaceStmt{Undo: r.accept(undo)} + r.expect(tablespace) + stmt.Name = ast.NewCIStr(r.parseIdentifier()) + stmt.Options = r.parseTablespaceOptions() + return stmt + case logfile: + // DropLogfileGroupStmt: "DROP" "LOGFILE" "GROUP" Identifier + // TablespaceOptions + r.advance() + r.advance() + r.expect(group) + stmt := &ast.DropLogfileGroupStmt{GroupName: ast.NewCIStr(r.parseIdentifier())} + stmt.Options = r.parseTablespaceOptions() + return stmt + case library: + // DropLibraryStmt: "DROP" "LIBRARY" IfExists TableName + r.advance() + r.advance() + ifExists := r.parseIfExists() + return &ast.DropLibraryStmt{IfExists: ifExists, LibraryName: r.parseTableName()} + case spatial: + // DropSpatialReferenceSystemStmt: "DROP" "SPATIAL" "REFERENCE" + // "SYSTEM" IfExists NUM + r.advance() + r.advance() + r.expectIdentLit("REFERENCE") + r.expect(system) + ifExists := r.parseIfExists() + return &ast.DropSpatialReferenceSystemStmt{ + IfExists: ifExists, + Srid: getUint64FromNUM(r.expect(intLit).item), + } + case masking: + // DropMaskingPolicyStmt: "DROP" "MASKING" "POLICY" IfExists + // PolicyName + r.advance() + r.advance() + r.expect(policy) + ifExists := r.parseIfExists() + return &ast.DropMaskingPolicyStmt{ + IfExists: ifExists, + PolicyName: ast.NewCIStr(r.parseIdentifier()), + } default: r.unsupported(fmt.Sprintf("DROP %s", r.at(r.i+1).lit)) } diff --git a/parser/parse_func.go b/parser/parse_func.go index e8a108c..8051151 100644 --- a/parser/parse_func.go +++ b/parser/parse_func.go @@ -101,6 +101,25 @@ func (r *rdParser) parseSimpleExprAtom() ast.ExprNode { return result } return r.setOrigin(r.parseSubSelect(), start) + case jsonDualityObject: + // SimpleExpr: "JSON_DUALITY_OBJECT" '(' [stringLit ':' Expression + // (',' stringLit ':' Expression)*] ')' — the select-list + // constructor of JSON duality views (MySQL 26.7 §15.1.14). + r.advance() + r.expect(int('(')) + x := &ast.JSONDualityObjectExpr{} + if r.tok() != int(')') { + for { + key := r.expect(stringLit).lit + r.expect(int(':')) + x.Items = append(x.Items, &ast.JSONDualityObjectItem{Key: key, Value: r.parseExpression()}) + if !r.accept(int(',')) { + break + } + } + } + r.expect(int(')')) + return r.setOrigin(x, start) case row: // SimpleExpr: "ROW" '(' ExpressionList ',' Expression ')' r.advance() diff --git a/parser/parse_mysql_admin.go b/parser/parse_mysql_admin.go index a253990..d75826f 100644 --- a/parser/parse_mysql_admin.go +++ b/parser/parse_mysql_admin.go @@ -126,19 +126,15 @@ func (r *rdParser) parseRepairTablesStmt() ast.StmtNode { } } -// parseCreateLoadableFunctionStmt implements CreateLoadableFunctionStmt -// (the CREATE FUNCTION statement for loadable functions; stored -// functions do not parse): +// finishCreateLoadableFunctionStmt implements the rest of +// CreateLoadableFunctionStmt (the CREATE FUNCTION statement for +// loadable functions) after parseCreateFunctionFamily has parsed +// through the function name: // // "CREATE" ["AGGREGATE"] "FUNCTION" IfNotExists Identifier // "RETURNS" ("STRING" | "INTEGER" | "INT" | "REAL" | "DECIMAL") // "SONAME" stringLit -func (r *rdParser) parseCreateLoadableFunctionStmt() ast.StmtNode { - r.expect(create) - aggregateOpt := r.accept(aggregate) - r.expect(function) - ifNotExists := r.parseIfNotExists() - name := r.parseIdentifier() +func (r *rdParser) finishCreateLoadableFunctionStmt(aggregateOpt, ifNotExists bool, name ast.CIStr) ast.StmtNode { r.expect(returns) var returnType ast.LoadableFunctionReturnType switch r.tok() { @@ -158,7 +154,7 @@ func (r *rdParser) parseCreateLoadableFunctionStmt() ast.StmtNode { return &ast.CreateLoadableFunctionStmt{ IfNotExists: ifNotExists, Aggregate: aggregateOpt, - FunctionName: ast.NewCIStr(name), + FunctionName: name, ReturnType: returnType, SoName: r.expect(stringLit).lit, } diff --git a/parser/parse_mysql_ddl.go b/parser/parse_mysql_ddl.go new file mode 100644 index 0000000..e13c9dc --- /dev/null +++ b/parser/parse_mysql_ddl.go @@ -0,0 +1,889 @@ +// 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 + +// The MySQL data definition statements that postdate the goyacc grammar +// (MySQL 26.7 §15.1): events, triggers, stored functions (and the ALTER +// forms of stored routines), ALTER VIEW, servers, tablespaces, logfile +// groups, spatial reference systems, libraries, and JSON duality views. +// The productions are written from the MySQL 26.7 reference manual in +// the same style as parser.y. The CREATE/ALTER/DROP statement heads +// dispatch here from parse_create_table.go, parse_alter.go, and +// parse_drop.go. + +import ( + "strings" + + "github.com/sqlc-dev/marino/ast" + "github.com/sqlc-dev/marino/auth" +) + +// parseDefinerOpt implements DefinerOpt: empty | "DEFINER" eq Username. +// It returns nil for the empty alternative. +func (r *rdParser) parseDefinerOpt() *auth.UserIdentity { + if r.tok() != definer { + return nil + } + r.advance() + r.expect(eq) + return r.parseUsername() +} + +// peekPastDefiner reports the token following the "DEFINER" eq Username +// clause that begins one token past the cursor (which sits on CREATE or +// ALTER), consuming nothing. It returns -1 when the clause does not +// parse; the caller's default alternative then reports the error. +func (r *rdParser) peekPastDefiner() int { + tok := -1 + m := r.mark() + r.try(func() { + r.advance() // CREATE or ALTER + r.advance() // DEFINER + r.expect(eq) + r.parseUsername() + tok = r.tok() + }) + r.rewind(m) + return tok +} + +// expectIdentLit consumes the current token, which must occupy +// identifier position and match name case-insensitively. It is used for +// the words of these productions that are not keywords (non-reserved in +// MySQL). +func (r *rdParser) expectIdentLit(name string) { + if !isIdentifierTok(r.tok()) || !strings.EqualFold(r.cur().lit, name) { + r.syntaxError() + } + r.advance() +} + +/* + * Events + */ + +// parseEventSchedule implements the schedule of ON SCHEDULE: +// +// "AT" Expression +// | "EVERY" Expression TimeUnit ["STARTS" Expression] +// ["ENDS" Expression] +func (r *rdParser) parseEventSchedule() *ast.EventSchedule { + if r.accept(at) { + return &ast.EventSchedule{At: r.parseExpression()} + } + r.expect(every) + sched := &ast.EventSchedule{Every: r.parseExpression(), Unit: r.parseTimeUnit()} + if r.accept(starts) { + sched.Starts = r.parseExpression() + } + if r.accept(ends) { + sched.Ends = r.parseExpression() + } + return sched +} + +// parseEventCompletion implements the ON COMPLETION clause, with ON +// already consumed: "COMPLETION" ["NOT"] "PRESERVE". +func (r *rdParser) parseEventCompletion() ast.EventCompletion { + r.expect(completion) + completionV := ast.EventCompletionPreserve + if r.accept(not) { + completionV = ast.EventCompletionNotPreserve + } + r.expect(preserve) + return completionV +} + +// parseEventStateOpt implements the event state clause: +// empty | "ENABLE" | "DISABLE" ["ON" "REPLICA"]. +func (r *rdParser) parseEventStateOpt() ast.EventState { + switch r.tok() { + case enable: + r.advance() + return ast.EventStateEnable + case disable: + r.advance() + if r.accept(on) { + r.expect(replica) + return ast.EventStateDisableOnReplica + } + return ast.EventStateDisable + } + return ast.EventStateDefault +} + +// parseEventBody parses an event, trigger, or function body statement +// and records its source text the way CreateProcedureStmt does. +func (r *rdParser) parseRoutineBody() ast.StmtNode { + bodyStart := r.cur().offset + body := r.parseProcedureProcStmt() + r.p.setNodeText(body, strings.TrimSpace(r.src[bodyStart:r.cur().offset])) + return body +} + +// parseCreateEventStmt implements CreateEventStmt: +// +// "CREATE" DefinerOpt "EVENT" IfNotExists TableName +// "ON" "SCHEDULE" EventSchedule ["ON" "COMPLETION" ["NOT"] +// "PRESERVE"] EventStateOpt ["COMMENT" stringLit] +// "DO" ProcedureProcStmt +func (r *rdParser) parseCreateEventStmt() ast.StmtNode { + r.expect(create) + stmt := &ast.CreateEventStmt{Definer: r.parseDefinerOpt()} + r.expect(event) + stmt.IfNotExists = r.parseIfNotExists() + stmt.EventName = r.parseTableName() + r.expect(on) + r.expect(schedule) + stmt.Schedule = r.parseEventSchedule() + if r.accept(on) { + stmt.Completion = r.parseEventCompletion() + } + stmt.State = r.parseEventStateOpt() + if r.accept(comment) { + s := r.expect(stringLit).lit + stmt.Comment = &s + } + r.expect(do) + stmt.Body = r.parseRoutineBody() + return stmt +} + +// parseAlterEventStmt implements AlterEventStmt: +// +// "ALTER" DefinerOpt "EVENT" TableName ["ON" "SCHEDULE" EventSchedule] +// ["ON" "COMPLETION" ["NOT"] "PRESERVE"] ["RENAME" "TO" TableName] +// EventStateOpt ["COMMENT" stringLit] ["DO" ProcedureProcStmt] +func (r *rdParser) parseAlterEventStmt() ast.StmtNode { + r.expect(alter) + stmt := &ast.AlterEventStmt{Definer: r.parseDefinerOpt()} + r.expect(event) + stmt.EventName = r.parseTableName() + if r.tok() == on && r.la(1) == schedule { + r.advance() + r.advance() + stmt.Schedule = r.parseEventSchedule() + } + if r.tok() == on && r.la(1) == completion { + r.advance() + stmt.Completion = r.parseEventCompletion() + } + if r.accept(rename) { + r.expect(to) + stmt.RenameTo = r.parseTableName() + } + stmt.State = r.parseEventStateOpt() + if r.accept(comment) { + s := r.expect(stringLit).lit + stmt.Comment = &s + } + if r.accept(do) { + stmt.Body = r.parseRoutineBody() + } + return stmt +} + +/* + * Triggers + */ + +// parseCreateTriggerStmt implements CreateTriggerStmt: +// +// "CREATE" DefinerOpt "TRIGGER" IfNotExists TableName +// ("BEFORE" | "AFTER") ("INSERT" | "UPDATE" | "DELETE") +// "ON" TableName "FOR" "EACH" "ROW" +// [("FOLLOWS" | "PRECEDES") Identifier] ProcedureProcStmt +func (r *rdParser) parseCreateTriggerStmt() ast.StmtNode { + r.expect(create) + stmt := &ast.CreateTriggerStmt{Definer: r.parseDefinerOpt()} + r.expect(trigger) + stmt.IfNotExists = r.parseIfNotExists() + stmt.TriggerName = r.parseTableName() + switch r.tok() { + case before: + stmt.TriggerTime = ast.TriggerTimeBefore + case after: + stmt.TriggerTime = ast.TriggerTimeAfter + default: + r.syntaxError() + } + r.advance() + switch r.tok() { + case insert: + stmt.TriggerEvent = ast.TriggerEventInsert + case update: + stmt.TriggerEvent = ast.TriggerEventUpdate + case deleteKwd: + stmt.TriggerEvent = ast.TriggerEventDelete + default: + r.syntaxError() + } + r.advance() + r.expect(on) + stmt.Table = r.parseTableName() + r.expect(forKwd) + r.expect(each) + r.expect(row) + switch r.tok() { + case follows: + r.advance() + stmt.Order = &ast.TriggerOrder{OtherTrigger: ast.NewCIStr(r.parseIdentifier())} + case precedes: + r.advance() + stmt.Order = &ast.TriggerOrder{Precedes: true, OtherTrigger: ast.NewCIStr(r.parseIdentifier())} + } + stmt.Body = r.parseRoutineBody() + return stmt +} + +/* + * Stored routines + */ + +// parseRoutineCharacteristics implements the characteristic list of the +// stored routine statements: +// +// ("COMMENT" stringLit | "LANGUAGE" (Identifier | "SQL") +// | ["NOT"] "DETERMINISTIC" | "CONTAINS" "SQL" | "NO" "SQL" +// | "READS" "SQL" "DATA" | "MODIFIES" "SQL" "DATA" +// | "SQL" "SECURITY" ("DEFINER" | "INVOKER"))* +// +// MySQL rejects [NOT] DETERMINISTIC in the ALTER statements at a later +// stage; the list is shared here and leaves that to semantics. +func (r *rdParser) parseRoutineCharacteristics() ast.RoutineCharacteristics { + var c ast.RoutineCharacteristics + for { + switch r.tok() { + case comment: + r.advance() + s := r.expect(stringLit).lit + c.Comment = &s + case language: + r.advance() + if r.tok() == sql { + r.advance() + c.Language = ast.NewCIStr("SQL") + } else { + c.Language = ast.NewCIStr(strings.ToUpper(r.parseIdentifier())) + } + case deterministic: + r.advance() + c.Determinism = ast.RoutineDeterministic + case not: + if r.la(1) != deterministic { + return c + } + r.advance() + r.advance() + c.Determinism = ast.RoutineNotDeterministic + case contains: + r.advance() + r.expect(sql) + c.DataAccess = ast.RoutineContainsSQL + case no: + if r.la(1) != sql { + return c + } + r.advance() + r.advance() + c.DataAccess = ast.RoutineNoSQL + case reads: + r.advance() + r.expect(sql) + r.expect(data) + c.DataAccess = ast.RoutineReadsSQLData + case modifies: + r.advance() + r.expect(sql) + r.expect(data) + c.DataAccess = ast.RoutineModifiesSQLData + case sql: + if r.la(1) != security { + return c + } + r.advance() + r.advance() + var s ast.ViewSecurity + switch r.tok() { + case definer: + s = ast.SecurityDefiner + case invoker: + s = ast.SecurityInvoker + default: + r.syntaxError() + } + r.advance() + c.Security = &s + default: + return c + } + } +} + +// parseCreateFunctionFamily dispatches CREATE FUNCTION between the +// stored function form (CreateFunctionStmt, recognized by its mandatory +// parameter list) and the loadable function form +// (CreateLoadableFunctionStmt, whose name is followed directly by +// RETURNS): +// +// "CREATE" DefinerOpt ["AGGREGATE"] "FUNCTION" IfNotExists ... +func (r *rdParser) parseCreateFunctionFamily() ast.StmtNode { + r.expect(create) + definer := r.parseDefinerOpt() + aggregateOpt := definer == nil && r.accept(aggregate) + r.expect(function) + ifNotExists := r.parseIfNotExists() + name := r.parseTableName() + if !aggregateOpt && r.tok() == int('(') { + return r.finishCreateStoredFunctionStmt(definer, ifNotExists, name) + } + if definer != nil || name.Schema.O != "" { + // The loadable form takes neither a definer nor a qualified name. + r.syntaxError() + } + return r.finishCreateLoadableFunctionStmt(aggregateOpt, ifNotExists, name.Name) +} + +// finishCreateStoredFunctionStmt implements the rest of +// CreateFunctionStmt after the function name: +// +// '(' [Identifier Type (',' Identifier Type)*] ')' "RETURNS" Type +// RoutineCharacteristics ProcedureProcStmt +func (r *rdParser) finishCreateStoredFunctionStmt(definer *auth.UserIdentity, ifNotExists bool, name *ast.TableName) ast.StmtNode { + stmt := &ast.CreateFunctionStmt{ + IfNotExists: ifNotExists, + Definer: definer, + FunctionName: name, + } + r.expect(int('(')) + if r.tok() != int(')') { + for { + param := &ast.FunctionParam{ParamName: r.parseIdentifier()} + param.ParamType = r.parseType() + stmt.Params = append(stmt.Params, param) + if !r.accept(int(',')) { + break + } + } + } + r.expect(int(')')) + r.expect(returns) + stmt.ReturnType = r.parseType() + stmt.Characteristics = r.parseRoutineCharacteristics() + stmt.Body = r.parseRoutineBody() + return stmt +} + +// parseAlterFunctionStmt implements AlterFunctionStmt: +// "ALTER" "FUNCTION" TableName RoutineCharacteristics. +func (r *rdParser) parseAlterFunctionStmt() ast.StmtNode { + r.expect(alter) + r.expect(function) + return &ast.AlterFunctionStmt{ + FunctionName: r.parseTableName(), + Characteristics: r.parseRoutineCharacteristics(), + } +} + +// parseAlterProcedureStmt implements AlterProcedureStmt: +// "ALTER" "PROCEDURE" TableName RoutineCharacteristics. +func (r *rdParser) parseAlterProcedureStmt() ast.StmtNode { + r.expect(alter) + r.expect(procedure) + return &ast.AlterProcedureStmt{ + ProcedureName: r.parseTableName(), + Characteristics: r.parseRoutineCharacteristics(), + } +} + +/* + * ALTER VIEW + */ + +// parseAlterViewStmt implements AlterViewStmt: +// +// "ALTER" ViewAlgorithm ViewDefiner ViewSQLSecurity "VIEW" ViewName +// ViewFieldList "AS" CreateViewSelectOpt ViewCheckOption +// +// mirroring parseCreateViewStmt, including its recording of the select +// statement's source text. +func (r *rdParser) parseAlterViewStmt() ast.StmtNode { + r.expect(alter) + algorithmV := ast.AlgorithmUndefined + if r.tok() == algorithm { + r.advance() + r.expect(eq) + switch r.tok() { + case undefined: + algorithmV = ast.AlgorithmUndefined + case merge: + algorithmV = ast.AlgorithmMerge + case temptable: + algorithmV = ast.AlgorithmTemptable + default: + r.syntaxError() + } + r.advance() + } + definerV := &auth.UserIdentity{CurrentUser: true} + if d := r.parseDefinerOpt(); d != nil { + definerV = d + } + securityV := ast.SecurityDefiner + if r.tok() == sql { + r.advance() + r.expect(security) + switch r.tok() { + case definer: + securityV = ast.SecurityDefiner + case invoker: + securityV = ast.SecurityInvoker + default: + r.syntaxError() + } + r.advance() + } + r.expect(view) + viewName := r.parseTableName() + var cols []ast.CIStr + if r.tok() == int('(') { + r.advance() + cols = []ast.CIStr{ast.NewCIStr(r.parseIdentifier())} + for r.accept(int(',')) { + cols = append(cols, ast.NewCIStr(r.parseIdentifier())) + } + r.expect(int(')')) + } + r.expect(as) + startOffset := r.cur().offset + selStmt := r.parseCreateViewSelectOpt() + endOffset := r.cur().offset + x := &ast.AlterViewStmt{ + ViewName: viewName, + Select: selStmt, + Algorithm: algorithmV, + Definer: definerV, + Security: securityV, + } + if cols != nil { + x.Cols = cols + } + if r.tok() == with { + r.advance() + switch r.tok() { + case cascaded: + x.CheckOption = ast.CheckOptionCascaded + case local: + x.CheckOption = ast.CheckOptionLocal + default: + r.syntaxError() + } + r.advance() + r.expect(check) + r.expect(option) + } else { + x.CheckOption = ast.CheckOptionCascaded + } + r.p.setNodeText(selStmt, strings.TrimSpace(r.src[startOffset:endOffset])) + return x +} + +/* + * Servers + */ + +// parseServerOptions implements the OPTIONS clause of CREATE/ALTER +// SERVER: "OPTIONS" '(' ServerOption (',' ServerOption)* ')'. +func (r *rdParser) parseServerOptions() []*ast.ServerOption { + r.expect(options) + r.expect(int('(')) + opts := []*ast.ServerOption{r.parseServerOption()} + for r.accept(int(',')) { + opts = append(opts, r.parseServerOption()) + } + r.expect(int(')')) + return opts +} + +// parseServerOption implements ServerOption: +// +// ("HOST" | "DATABASE" | "USER" | "PASSWORD" | "SOCKET" | "OWNER") +// stringLit +// | "PORT" NUM +// +// HOST, SOCKET, OWNER, and PORT are not keywords and are matched in +// identifier position. +func (r *rdParser) parseServerOption() *ast.ServerOption { + var name string + switch { + case r.tok() == user || r.tok() == password || r.tok() == database: + name = strings.ToUpper(r.cur().lit) + case isIdentifierTok(r.tok()): + name = strings.ToUpper(r.cur().lit) + switch name { + case "HOST", "SOCKET", "OWNER", "PORT": + default: + r.syntaxError() + } + default: + r.syntaxError() + } + r.advance() + if name == "PORT" { + return &ast.ServerOption{Name: name, Value: ast.NewValueExpr(r.expect(intLit).item, "", "")} + } + return &ast.ServerOption{Name: name, Value: ast.NewValueExpr(r.expect(stringLit).lit, "", "")} +} + +// parseCreateServerStmt implements CreateServerStmt: +// +// "CREATE" "SERVER" Identifier "FOREIGN" "DATA" "WRAPPER" Identifier +// ServerOptions +func (r *rdParser) parseCreateServerStmt() ast.StmtNode { + r.expect(create) + r.expect(server) + stmt := &ast.CreateServerStmt{ServerName: ast.NewCIStr(r.parseIdentifier())} + r.expect(foreign) + r.expect(data) + r.expect(wrapper) + stmt.Wrapper = ast.NewCIStr(r.parseIdentifier()) + stmt.Options = r.parseServerOptions() + return stmt +} + +// parseAlterServerStmt implements AlterServerStmt: +// "ALTER" "SERVER" Identifier ServerOptions. +func (r *rdParser) parseAlterServerStmt() ast.StmtNode { + r.expect(alter) + r.expect(server) + return &ast.AlterServerStmt{ + ServerName: ast.NewCIStr(r.parseIdentifier()), + Options: r.parseServerOptions(), + } +} + +/* + * Tablespaces and logfile groups + */ + +// parseTablespaceOptions implements the option list shared by the +// tablespace and logfile group statements: +// +// (("INITIAL_SIZE" | "MAX_SIZE" | "EXTENT_SIZE" | "FILE_BLOCK_SIZE" +// | "UNDO_BUFFER_SIZE" | "REDO_BUFFER_SIZE" | "AUTOEXTEND_SIZE" +// | "NODEGROUP") EqOpt LengthNum +// | ("ENCRYPTION" | "COMMENT" | "ENGINE_ATTRIBUTE") EqOpt stringLit +// | "ENGINE" EqOpt (Identifier | stringLit) +// | "WAIT")* +// +// The size option names that are not keywords are matched in identifier +// position. +func (r *rdParser) parseTablespaceOptions() []*ast.TablespaceOption { + var opts []*ast.TablespaceOption + for { + switch r.tok() { + case autoextendSize: + r.advance() + r.parseEqOpt() + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionAutoextendSize, UintValue: getUint64FromNUM(r.expect(intLit).item)}) + case nodegroup: + r.advance() + r.parseEqOpt() + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionNodegroup, UintValue: getUint64FromNUM(r.expect(intLit).item)}) + case wait: + r.advance() + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionWait}) + case encryption: + r.advance() + r.parseEqOpt() + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionEncryption, StrValue: r.expect(stringLit).lit}) + case comment: + r.advance() + r.parseEqOpt() + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionComment, StrValue: r.expect(stringLit).lit}) + case engine_attribute: + r.advance() + r.parseEqOpt() + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionEngineAttribute, StrValue: r.expect(stringLit).lit}) + case engine: + r.advance() + r.parseEqOpt() + var name string + if r.tok() == stringLit { + name = r.cur().lit + r.advance() + } else { + name = r.parseIdentifier() + } + opts = append(opts, &ast.TablespaceOption{Tp: ast.TablespaceOptionEngine, StrValue: name}) + default: + var tp ast.TablespaceOptionType + switch { + case !isIdentifierTok(r.tok()): + return opts + case strings.EqualFold(r.cur().lit, "INITIAL_SIZE"): + tp = ast.TablespaceOptionInitialSize + case strings.EqualFold(r.cur().lit, "MAX_SIZE"): + tp = ast.TablespaceOptionMaxSize + case strings.EqualFold(r.cur().lit, "EXTENT_SIZE"): + tp = ast.TablespaceOptionExtentSize + case strings.EqualFold(r.cur().lit, "FILE_BLOCK_SIZE"): + tp = ast.TablespaceOptionFileBlockSize + case strings.EqualFold(r.cur().lit, "UNDO_BUFFER_SIZE"): + tp = ast.TablespaceOptionUndoBufferSize + case strings.EqualFold(r.cur().lit, "REDO_BUFFER_SIZE"): + tp = ast.TablespaceOptionRedoBufferSize + default: + return opts + } + r.advance() + r.parseEqOpt() + opts = append(opts, &ast.TablespaceOption{Tp: tp, UintValue: getUint64FromNUM(r.expect(intLit).item)}) + } + } +} + +// parseCreateTablespaceStmt implements CreateTablespaceStmt: +// +// "CREATE" ["UNDO"] "TABLESPACE" Identifier +// ["ADD" "DATAFILE" stringLit] ["USE" "LOGFILE" "GROUP" Identifier] +// TablespaceOptions +func (r *rdParser) parseCreateTablespaceStmt() ast.StmtNode { + r.expect(create) + stmt := &ast.CreateTablespaceStmt{Undo: r.accept(undo)} + r.expect(tablespace) + stmt.Name = ast.NewCIStr(r.parseIdentifier()) + if r.accept(add) { + r.expect(datafile) + stmt.Datafile = r.expect(stringLit).lit + } + if r.accept(use) { + r.expect(logfile) + r.expect(group) + stmt.UseLogfileGroup = ast.NewCIStr(r.parseIdentifier()) + } + stmt.Options = r.parseTablespaceOptions() + return stmt +} + +// parseAlterTablespaceStmt implements AlterTablespaceStmt: +// +// "ALTER" ["UNDO"] "TABLESPACE" Identifier +// [("ADD" | "DROP") "DATAFILE" stringLit | "RENAME" "TO" Identifier +// | "SET" ("ACTIVE" | "INACTIVE")] TablespaceOptions +// +// ACTIVE and INACTIVE are not keywords and are matched in identifier +// position. +func (r *rdParser) parseAlterTablespaceStmt() ast.StmtNode { + r.expect(alter) + stmt := &ast.AlterTablespaceStmt{Undo: r.accept(undo)} + r.expect(tablespace) + stmt.Name = ast.NewCIStr(r.parseIdentifier()) + switch r.tok() { + case add: + r.advance() + r.expect(datafile) + stmt.Op = ast.AlterTablespaceAddDatafile + stmt.Datafile = r.expect(stringLit).lit + case drop: + r.advance() + r.expect(datafile) + stmt.Op = ast.AlterTablespaceDropDatafile + stmt.Datafile = r.expect(stringLit).lit + case rename: + r.advance() + r.expect(to) + stmt.Op = ast.AlterTablespaceRenameTo + stmt.NewName = ast.NewCIStr(r.parseIdentifier()) + case set: + r.advance() + if isIdentifierTok(r.tok()) && strings.EqualFold(r.cur().lit, "ACTIVE") { + stmt.Op = ast.AlterTablespaceSetActive + } else if isIdentifierTok(r.tok()) && strings.EqualFold(r.cur().lit, "INACTIVE") { + stmt.Op = ast.AlterTablespaceSetInactive + } else { + r.syntaxError() + } + r.advance() + } + stmt.Options = r.parseTablespaceOptions() + return stmt +} + +// parseCreateLogfileGroupStmt implements CreateLogfileGroupStmt: +// +// "CREATE" "LOGFILE" "GROUP" Identifier "ADD" "UNDOFILE" stringLit +// TablespaceOptions +func (r *rdParser) parseCreateLogfileGroupStmt() ast.StmtNode { + r.expect(create) + r.expect(logfile) + r.expect(group) + stmt := &ast.CreateLogfileGroupStmt{GroupName: ast.NewCIStr(r.parseIdentifier())} + r.expect(add) + r.expect(undofile) + stmt.Undofile = r.expect(stringLit).lit + stmt.Options = r.parseTablespaceOptions() + return stmt +} + +// parseAlterLogfileGroupStmt implements AlterLogfileGroupStmt: +// +// "ALTER" "LOGFILE" "GROUP" Identifier "ADD" "UNDOFILE" stringLit +// TablespaceOptions +func (r *rdParser) parseAlterLogfileGroupStmt() ast.StmtNode { + r.expect(alter) + r.expect(logfile) + r.expect(group) + stmt := &ast.AlterLogfileGroupStmt{GroupName: ast.NewCIStr(r.parseIdentifier())} + r.expect(add) + r.expect(undofile) + stmt.Undofile = r.expect(stringLit).lit + stmt.Options = r.parseTablespaceOptions() + return stmt +} + +/* + * Spatial reference systems + */ + +// parseCreateSpatialReferenceSystemStmt implements +// CreateSpatialReferenceSystemStmt: +// +// "CREATE" OrReplace "SPATIAL" "REFERENCE" "SYSTEM" IfNotExists NUM +// SRSAttribute* +// SRSAttribute: ("NAME" | "DEFINITION" | "DESCRIPTION") stringLit +// | "ORGANIZATION" stringLit "IDENTIFIED" "BY" NUM +// +// REFERENCE and the attribute names are not keywords and are matched in +// identifier position. +func (r *rdParser) parseCreateSpatialReferenceSystemStmt() ast.StmtNode { + r.expect(create) + stmt := &ast.CreateSpatialReferenceSystemStmt{} + if r.tok() == or { + r.advance() + r.expect(replace) + stmt.OrReplace = true + } + r.expect(spatial) + r.expectIdentLit("REFERENCE") + r.expect(system) + stmt.IfNotExists = r.parseIfNotExists() + stmt.Srid = getUint64FromNUM(r.expect(intLit).item) + for isIdentifierTok(r.tok()) { + attr := &ast.SRSAttribute{} + switch strings.ToUpper(r.cur().lit) { + case "NAME": + attr.Tp = ast.SRSAttributeName + case "DEFINITION": + attr.Tp = ast.SRSAttributeDefinition + case "ORGANIZATION": + attr.Tp = ast.SRSAttributeOrganization + case "DESCRIPTION": + attr.Tp = ast.SRSAttributeDescription + default: + r.syntaxError() + } + r.advance() + attr.StrValue = r.expect(stringLit).lit + if attr.Tp == ast.SRSAttributeOrganization { + r.expect(identified) + r.expect(by) + attr.OrgID = getUint64FromNUM(r.expect(intLit).item) + } + stmt.Attributes = append(stmt.Attributes, attr) + } + return stmt +} + +/* + * Libraries + */ + +// parseCreateLibraryStmt implements CreateLibraryStmt: +// +// "CREATE" OrReplace "LIBRARY" IfNotExists TableName +// "LANGUAGE" Identifier "AS" stringLit +func (r *rdParser) parseCreateLibraryStmt() ast.StmtNode { + r.expect(create) + stmt := &ast.CreateLibraryStmt{} + if r.tok() == or { + r.advance() + r.expect(replace) + stmt.OrReplace = true + } + r.expect(library) + stmt.IfNotExists = r.parseIfNotExists() + stmt.LibraryName = r.parseTableName() + r.expect(language) + stmt.Language = ast.NewCIStr(strings.ToUpper(r.parseIdentifier())) + r.expect(as) + stmt.Code = r.expect(stringLit).lit + return stmt +} + +// parseAlterLibraryStmt implements AlterLibraryStmt: +// "ALTER" "LIBRARY" TableName "COMMENT" stringLit. +func (r *rdParser) parseAlterLibraryStmt() ast.StmtNode { + r.expect(alter) + r.expect(library) + stmt := &ast.AlterLibraryStmt{LibraryName: r.parseTableName()} + r.expect(comment) + stmt.Comment = r.expect(stringLit).lit + return stmt +} + +/* + * JSON duality views + */ + +// parseCreateJSONDualityViewStmt implements CreateJSONDualityViewStmt: +// +// "CREATE" OrReplace "JSON" "DUALITY" "VIEW" IfNotExists TableName +// "AS" CreateViewSelectOpt +// +// recording the select statement's source text like CreateViewStmt. +func (r *rdParser) parseCreateJSONDualityViewStmt() ast.StmtNode { + r.expect(create) + stmt := &ast.CreateJSONDualityViewStmt{} + if r.tok() == or { + r.advance() + r.expect(replace) + stmt.OrReplace = true + } + r.expect(jsonType) + r.expect(duality) + r.expect(view) + stmt.IfNotExists = r.parseIfNotExists() + stmt.ViewName = r.parseTableName() + r.expect(as) + startOffset := r.cur().offset + stmt.Select = r.parseCreateViewSelectOpt() + r.p.setNodeText(stmt.Select, strings.TrimSpace(r.src[startOffset:r.cur().offset])) + return stmt +} + +// parseAlterJSONDualityViewStmt implements AlterJSONDualityViewStmt: +// "ALTER" "JSON" "DUALITY" "VIEW" TableName "AS" CreateViewSelectOpt. +func (r *rdParser) parseAlterJSONDualityViewStmt() ast.StmtNode { + r.expect(alter) + r.expect(jsonType) + r.expect(duality) + r.expect(view) + stmt := &ast.AlterJSONDualityViewStmt{ViewName: r.parseTableName()} + r.expect(as) + startOffset := r.cur().offset + stmt.Select = r.parseCreateViewSelectOpt() + r.p.setNodeText(stmt.Select, strings.TrimSpace(r.src[startOffset:r.cur().offset])) + return stmt +} diff --git a/parser/parse_procedure.go b/parser/parse_procedure.go index 9ae1676..79437ef 100644 --- a/parser/parse_procedure.go +++ b/parser/parse_procedure.go @@ -73,14 +73,15 @@ func (r *rdParser) parseProcedureCall() *ast.FuncCallExpr { // parseCreateProcedureStmt implements: // -// CreateProcedureStmt: "CREATE" "PROCEDURE" IfNotExists TableName '(' -// OptSpPdparams ')' ProcedureProcStmt +// CreateProcedureStmt: "CREATE" DefinerOpt "PROCEDURE" IfNotExists +// TableName '(' OptSpPdparams ')' ProcedureProcStmt // // The action records the body statement's source text — from the body's // first token through the reduce-time lookahead (parser.yylval.offset) — // and the parameter list's source text between the parentheses. func (r *rdParser) parseCreateProcedureStmt() ast.StmtNode { r.expect(create) + definerV := r.parseDefinerOpt() r.expect(procedure) ifNotExists := r.parseIfNotExists() procName := r.parseTableName() @@ -94,6 +95,7 @@ func (r *rdParser) parseCreateProcedureStmt() ast.StmtNode { ProcedureName: procName, ProcedureParam: params, ProcedureBody: body, + Definer: definerV, } r.p.setNodeText(body, strings.TrimSpace(r.src[bodyStart:r.cur().offset])) startOffset := lparen.offset @@ -192,6 +194,11 @@ func (r *rdParser) parseProcedureProcStmt() ast.StmtNode { // ProcedureLeave: "LEAVE" identifier r.advance() return &ast.ProcedureJump{Name: r.expect(identifier).lit, IsLeave: true} + case returnKwd: + // ReturnStmt: "RETURN" Expression — the routine_body form of + // stored functions; MySQL rejects it elsewhere at a later stage. + r.advance() + return &ast.ReturnStmt{Expr: r.parseExpression()} case identifier: if r.la(1) == int(':') { return r.parseProcedureLabeled() diff --git a/parser/testdata/parser/mysql_ddl/input.sql b/parser/testdata/parser/mysql_ddl/input.sql new file mode 100644 index 0000000..123769b --- /dev/null +++ b/parser/testdata/parser/mysql_ddl/input.sql @@ -0,0 +1,147 @@ +CREATE EVENT e ON SCHEDULE AT CURRENT_TIMESTAMP DO SELECT 1 +-- case +CREATE EVENT IF NOT EXISTS db1.e ON SCHEDULE AT '2026-09-01 00:00:00' + INTERVAL 1 DAY DO SELECT 1 +-- case +CREATE EVENT e ON SCHEDULE EVERY 2 HOUR STARTS CURRENT_TIMESTAMP ENDS CURRENT_TIMESTAMP + INTERVAL 1 WEEK ON COMPLETION PRESERVE DISABLE COMMENT 'cleanup' DO SELECT 1 +-- case +CREATE DEFINER = 'admin'@'localhost' EVENT e ON SCHEDULE EVERY 1 DAY ON COMPLETION NOT PRESERVE DISABLE ON REPLICA DO SELECT 1 +-- case +ALTER EVENT myevent ON SCHEDULE EVERY 2 HOUR +-- case +ALTER EVENT myevent ON SCHEDULE AT CURRENT_TIMESTAMP ON COMPLETION PRESERVE RENAME TO yourevent ENABLE COMMENT 'new comment' DO SELECT 1 +-- case +ALTER DEFINER = CURRENT_USER EVENT e COMMENT 'x' +-- case +DROP EVENT e +-- case +DROP EVENT IF EXISTS db1.e +-- case +CREATE TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET @x = 1 +-- case +CREATE TRIGGER IF NOT EXISTS trg AFTER UPDATE ON t FOR EACH ROW SET @x = 1 +-- case +CREATE TRIGGER trg AFTER DELETE ON t FOR EACH ROW FOLLOWS other_trg SET @x = 1 +-- case +CREATE DEFINER = 'admin'@'localhost' TRIGGER trg BEFORE UPDATE ON t FOR EACH ROW PRECEDES other_trg SET @x = 1 +-- case +CREATE TRIGGER `trg` BEFORE INSERT ON `t` FOR EACH ROW BEGIN SET @`x`=1;SET @`y`=2; END +-- case +DROP TRIGGER trg +-- case +DROP TRIGGER IF EXISTS db1.trg +-- case +CREATE FUNCTION f(x INT) RETURNS INT DETERMINISTIC RETURN x + 1 +-- case +CREATE FUNCTION f() RETURNS VARCHAR(64) COMMENT 'greets' LANGUAGE SQL NOT DETERMINISTIC READS SQL DATA SQL SECURITY INVOKER RETURN 'hi' +-- case +CREATE DEFINER = `admin`@`localhost` FUNCTION `db1`.`f`(`x` INT, `y` CHAR(4)) RETURNS INT CONTAINS SQL SQL SECURITY DEFINER BEGIN RETURN `x`; END +-- case +CREATE FUNCTION f() RETURNS INT MODIFIES SQL DATA RETURN 1 +-- case +CREATE FUNCTION f() RETURNS INT NO SQL RETURN 1 +-- case +ALTER FUNCTION myfunc COMMENT 'some comment' +-- case +ALTER FUNCTION myfunc LANGUAGE SQL READS SQL DATA SQL SECURITY INVOKER +-- case +DROP FUNCTION f +-- case +DROP FUNCTION IF EXISTS db1.f +-- case +ALTER PROCEDURE myproc COMMENT 'some comment' +-- case +ALTER PROCEDURE myproc CONTAINS SQL SQL SECURITY DEFINER +-- case +CREATE DEFINER = 'admin'@'localhost' PROCEDURE p() SELECT 1 +-- case +ALTER VIEW v AS SELECT 1 +-- case +ALTER ALGORITHM = MERGE DEFINER = 'admin'@'localhost' SQL SECURITY INVOKER VIEW v (a, b) AS SELECT 1, 2 WITH LOCAL CHECK OPTION +-- case +ALTER INSTANCE ROTATE INNODB MASTER KEY +-- case +ALTER INSTANCE ROTATE BINLOG MASTER KEY +-- case +ALTER INSTANCE RELOAD TLS +-- case +ALTER INSTANCE RELOAD TLS FOR CHANNEL mysql_admin NO ROLLBACK ON ERROR +-- case +ALTER INSTANCE RELOAD KEYRING +-- case +ALTER INSTANCE ENABLE INNODB REDO_LOG +-- case +ALTER INSTANCE DISABLE INNODB REDO_LOG +-- case +CREATE SERVER s FOREIGN DATA WRAPPER mysql OPTIONS (USER 'user', HOST 'host', DATABASE 'db') +-- case +CREATE SERVER s FOREIGN DATA WRAPPER mysql OPTIONS (HOST '127.0.0.1', PORT 3306, SOCKET '/tmp/mysql.sock', OWNER 'root', PASSWORD 'secret') +-- case +ALTER SERVER s OPTIONS (USER 'user') +-- case +DROP SERVER s +-- case +DROP SERVER IF EXISTS s +-- case +CREATE TABLESPACE ts ADD DATAFILE 'file.ibd' ENGINE = INNODB +-- case +CREATE TABLESPACE ts ADD DATAFILE 'file.dat' USE LOGFILE GROUP lg1 INITIAL_SIZE = 134217728 EXTENT_SIZE = 4194304 MAX_SIZE = 1073741824 AUTOEXTEND_SIZE = 67108864 NODEGROUP = 1 WAIT COMMENT = 'ndb tablespace' ENGINE = NDB +-- case +CREATE UNDO TABLESPACE undo_ts ADD DATAFILE 'undo.ibu' FILE_BLOCK_SIZE = 8192 ENCRYPTION = 'Y' ENGINE_ATTRIBUTE = '{}' +-- case +ALTER TABLESPACE ts ADD DATAFILE 'file.ibd' ENGINE = NDB +-- case +ALTER TABLESPACE ts DROP DATAFILE 'file.ibd' WAIT ENGINE = NDB +-- case +ALTER TABLESPACE ts RENAME TO ts2 +-- case +ALTER UNDO TABLESPACE undo_ts SET INACTIVE +-- case +ALTER UNDO TABLESPACE undo_ts SET ACTIVE +-- case +DROP TABLESPACE ts ENGINE = INNODB +-- case +DROP UNDO TABLESPACE undo_ts +-- case +CREATE LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' ENGINE = NDB +-- case +CREATE LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' INITIAL_SIZE = 33554432 UNDO_BUFFER_SIZE = 8388608 REDO_BUFFER_SIZE = 8388608 NODEGROUP = 1 WAIT COMMENT = 'ndb logs' ENGINE = NDB +-- case +ALTER LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' ENGINE = NDB +-- case +ALTER LOGFILE GROUP lg1 ADD UNDOFILE 'undo2.dat' INITIAL_SIZE = 33554432 WAIT ENGINE = NDB +-- case +DROP LOGFILE GROUP lg1 ENGINE = NDB +-- case +CREATE SPATIAL REFERENCE SYSTEM 5000 NAME 'my srs' DEFINITION 'GEOGCS[]' +-- case +CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 5000 NAME 'my srs' ORGANIZATION 'EPSG' IDENTIFIED BY 4326 DEFINITION 'GEOGCS[]' DESCRIPTION 'a geographic srs' +-- case +CREATE SPATIAL REFERENCE SYSTEM IF NOT EXISTS 5000 NAME 'my srs' DEFINITION 'GEOGCS[]' +-- case +DROP SPATIAL REFERENCE SYSTEM 5000 +-- case +DROP SPATIAL REFERENCE SYSTEM IF EXISTS 5000 +-- case +CREATE LIBRARY mylib LANGUAGE JAVASCRIPT AS 'export function f() { return 1 }' +-- case +CREATE OR REPLACE LIBRARY IF NOT EXISTS db1.mylib LANGUAGE JAVASCRIPT AS 'export function f() { return 1 }' +-- case +ALTER LIBRARY mylib COMMENT 'updated' +-- case +DROP LIBRARY mylib +-- case +DROP LIBRARY IF EXISTS db1.mylib +-- case +CREATE JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id) FROM t +-- case +CREATE OR REPLACE JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id, 'name' : t.name, 'child' : JSON_DUALITY_OBJECT('cid' : c.id)) FROM t +-- case +ALTER JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id) FROM t +-- case +CREATE MASKING POLICY p ON t (c) USING (mask_inner(c, 1, 1)) +-- case +DROP MASKING POLICY p +-- case +DROP MASKING POLICY IF EXISTS p +-- case +SELECT `at`, `every`, `starts`, `ends`, `server`, `options`, `wrapper`, `contains`, `duality`, `rotate`, `innodb` FROM t diff --git a/parser/testdata/parser/mysql_ddl/output.sql b/parser/testdata/parser/mysql_ddl/output.sql new file mode 100644 index 0000000..ab38616 --- /dev/null +++ b/parser/testdata/parser/mysql_ddl/output.sql @@ -0,0 +1,147 @@ +CREATE EVENT `e` ON SCHEDULE AT CURRENT_TIMESTAMP() DO SELECT 1 +-- case +CREATE EVENT IF NOT EXISTS `db1`.`e` ON SCHEDULE AT DATE_ADD(_UTF8MB4'2026-09-01 00:00:00', INTERVAL 1 DAY) DO SELECT 1 +-- case +CREATE EVENT `e` ON SCHEDULE EVERY 2 HOUR STARTS CURRENT_TIMESTAMP() ENDS DATE_ADD(CURRENT_TIMESTAMP(), INTERVAL 1 WEEK) ON COMPLETION PRESERVE DISABLE COMMENT 'cleanup' DO SELECT 1 +-- case +CREATE DEFINER = `admin`@`localhost` EVENT `e` ON SCHEDULE EVERY 1 DAY ON COMPLETION NOT PRESERVE DISABLE ON REPLICA DO SELECT 1 +-- case +ALTER EVENT `myevent` ON SCHEDULE EVERY 2 HOUR +-- case +ALTER EVENT `myevent` ON SCHEDULE AT CURRENT_TIMESTAMP() ON COMPLETION PRESERVE RENAME TO `yourevent` ENABLE COMMENT 'new comment' DO SELECT 1 +-- case +ALTER DEFINER = CURRENT_USER EVENT `e` COMMENT 'x' +-- case +DROP EVENT `e` +-- case +DROP EVENT IF EXISTS `db1`.`e` +-- case +CREATE TRIGGER `trg` BEFORE INSERT ON `t` FOR EACH ROW SET @`x`=1 +-- case +CREATE TRIGGER IF NOT EXISTS `trg` AFTER UPDATE ON `t` FOR EACH ROW SET @`x`=1 +-- case +CREATE TRIGGER `trg` AFTER DELETE ON `t` FOR EACH ROW FOLLOWS `other_trg` SET @`x`=1 +-- case +CREATE DEFINER = `admin`@`localhost` TRIGGER `trg` BEFORE UPDATE ON `t` FOR EACH ROW PRECEDES `other_trg` SET @`x`=1 +-- case +CREATE TRIGGER `trg` BEFORE INSERT ON `t` FOR EACH ROW BEGIN SET @`x`=1;SET @`y`=2; END +-- case +DROP TRIGGER `trg` +-- case +DROP TRIGGER IF EXISTS `db1`.`trg` +-- case +CREATE FUNCTION `f`(`x` INT) RETURNS INT DETERMINISTIC RETURN `x`+1 +-- case +CREATE FUNCTION `f`() RETURNS VARCHAR(64) COMMENT 'greets' LANGUAGE SQL NOT DETERMINISTIC READS SQL DATA SQL SECURITY INVOKER RETURN _UTF8MB4'hi' +-- case +CREATE DEFINER = `admin`@`localhost` FUNCTION `db1`.`f`(`x` INT, `y` CHAR(4)) RETURNS INT CONTAINS SQL SQL SECURITY DEFINER BEGIN RETURN `x`; END +-- case +CREATE FUNCTION `f`() RETURNS INT MODIFIES SQL DATA RETURN 1 +-- case +CREATE FUNCTION `f`() RETURNS INT NO SQL RETURN 1 +-- case +ALTER FUNCTION `myfunc` COMMENT 'some comment' +-- case +ALTER FUNCTION `myfunc` LANGUAGE SQL READS SQL DATA SQL SECURITY INVOKER +-- case +DROP FUNCTION `f` +-- case +DROP FUNCTION IF EXISTS `db1`.`f` +-- case +ALTER PROCEDURE `myproc` COMMENT 'some comment' +-- case +ALTER PROCEDURE `myproc` CONTAINS SQL SQL SECURITY DEFINER +-- case +CREATE DEFINER = `admin`@`localhost` PROCEDURE `p`() SELECT 1 +-- case +ALTER ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 +-- case +ALTER ALGORITHM = MERGE DEFINER = `admin`@`localhost` SQL SECURITY INVOKER VIEW `v` (`a`,`b`) AS SELECT 1,2 WITH LOCAL CHECK OPTION +-- case +ALTER INSTANCE ROTATE INNODB MASTER KEY +-- case +ALTER INSTANCE ROTATE BINLOG MASTER KEY +-- case +ALTER INSTANCE RELOAD TLS +-- case +ALTER INSTANCE RELOAD TLS FOR CHANNEL `mysql_admin` NO ROLLBACK ON ERROR +-- case +ALTER INSTANCE RELOAD KEYRING +-- case +ALTER INSTANCE ENABLE INNODB REDO_LOG +-- case +ALTER INSTANCE DISABLE INNODB REDO_LOG +-- case +CREATE SERVER `s` FOREIGN DATA WRAPPER `mysql` OPTIONS (USER 'user', HOST 'host', DATABASE 'db') +-- case +CREATE SERVER `s` FOREIGN DATA WRAPPER `mysql` OPTIONS (HOST '127.0.0.1', PORT 3306, SOCKET '/tmp/mysql.sock', OWNER 'root', PASSWORD 'secret') +-- case +ALTER SERVER `s` OPTIONS (USER 'user') +-- case +DROP SERVER `s` +-- case +DROP SERVER IF EXISTS `s` +-- case +CREATE TABLESPACE `ts` ADD DATAFILE 'file.ibd' ENGINE = INNODB +-- case +CREATE TABLESPACE `ts` ADD DATAFILE 'file.dat' USE LOGFILE GROUP `lg1` INITIAL_SIZE = 134217728 EXTENT_SIZE = 4194304 MAX_SIZE = 1073741824 AUTOEXTEND_SIZE = 67108864 NODEGROUP = 1 WAIT COMMENT = 'ndb tablespace' ENGINE = NDB +-- case +CREATE UNDO TABLESPACE `undo_ts` ADD DATAFILE 'undo.ibu' FILE_BLOCK_SIZE = 8192 ENCRYPTION = 'Y' ENGINE_ATTRIBUTE = '{}' +-- case +ALTER TABLESPACE `ts` ADD DATAFILE 'file.ibd' ENGINE = NDB +-- case +ALTER TABLESPACE `ts` DROP DATAFILE 'file.ibd' WAIT ENGINE = NDB +-- case +ALTER TABLESPACE `ts` RENAME TO `ts2` +-- case +ALTER UNDO TABLESPACE `undo_ts` SET INACTIVE +-- case +ALTER UNDO TABLESPACE `undo_ts` SET ACTIVE +-- case +DROP TABLESPACE `ts` ENGINE = INNODB +-- case +DROP UNDO TABLESPACE `undo_ts` +-- case +CREATE LOGFILE GROUP `lg1` ADD UNDOFILE 'undo.dat' ENGINE = NDB +-- case +CREATE LOGFILE GROUP `lg1` ADD UNDOFILE 'undo.dat' INITIAL_SIZE = 33554432 UNDO_BUFFER_SIZE = 8388608 REDO_BUFFER_SIZE = 8388608 NODEGROUP = 1 WAIT COMMENT = 'ndb logs' ENGINE = NDB +-- case +ALTER LOGFILE GROUP `lg1` ADD UNDOFILE 'undo.dat' ENGINE = NDB +-- case +ALTER LOGFILE GROUP `lg1` ADD UNDOFILE 'undo2.dat' INITIAL_SIZE = 33554432 WAIT ENGINE = NDB +-- case +DROP LOGFILE GROUP `lg1` ENGINE = NDB +-- case +CREATE SPATIAL REFERENCE SYSTEM 5000 NAME 'my srs' DEFINITION 'GEOGCS[]' +-- case +CREATE OR REPLACE SPATIAL REFERENCE SYSTEM 5000 NAME 'my srs' ORGANIZATION 'EPSG' IDENTIFIED BY 4326 DEFINITION 'GEOGCS[]' DESCRIPTION 'a geographic srs' +-- case +CREATE SPATIAL REFERENCE SYSTEM IF NOT EXISTS 5000 NAME 'my srs' DEFINITION 'GEOGCS[]' +-- case +DROP SPATIAL REFERENCE SYSTEM 5000 +-- case +DROP SPATIAL REFERENCE SYSTEM IF EXISTS 5000 +-- case +CREATE LIBRARY `mylib` LANGUAGE JAVASCRIPT AS 'export function f() { return 1 }' +-- case +CREATE OR REPLACE LIBRARY IF NOT EXISTS `db1`.`mylib` LANGUAGE JAVASCRIPT AS 'export function f() { return 1 }' +-- case +ALTER LIBRARY `mylib` COMMENT 'updated' +-- case +DROP LIBRARY `mylib` +-- case +DROP LIBRARY IF EXISTS `db1`.`mylib` +-- case +CREATE JSON DUALITY VIEW `jdv` AS SELECT JSON_DUALITY_OBJECT('id' : `t`.`id`) FROM `t` +-- case +CREATE OR REPLACE JSON DUALITY VIEW `jdv` AS SELECT JSON_DUALITY_OBJECT('id' : `t`.`id`, 'name' : `t`.`name`, 'child' : JSON_DUALITY_OBJECT('cid' : `c`.`id`)) FROM `t` +-- case +ALTER JSON DUALITY VIEW `jdv` AS SELECT JSON_DUALITY_OBJECT('id' : `t`.`id`) FROM `t` +-- case +CREATE MASKING POLICY `p` ON `t` (`c`) USING (MASK_INNER(`c`, 1, 1)) +-- case +DROP MASKING POLICY `p` +-- case +DROP MASKING POLICY IF EXISTS `p` +-- case +SELECT `at`,`every`,`starts`,`ends`,`server`,`options`,`wrapper`,`contains`,`duality`,`rotate`,`innodb` FROM `t` diff --git a/parser/testdata/parser/mysql_unsupported_ddl/input.sql b/parser/testdata/parser/mysql_unsupported_ddl/input.sql deleted file mode 100644 index 275bd3b..0000000 --- a/parser/testdata/parser/mysql_unsupported_ddl/input.sql +++ /dev/null @@ -1,57 +0,0 @@ -ALTER EVENT myevent ON SCHEDULE EVERY 2 HOUR --- case -ALTER FUNCTION myfunc COMMENT 'some comment' --- case -ALTER INSTANCE ROTATE INNODB MASTER KEY --- case -ALTER JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id) FROM t --- case -ALTER LIBRARY mylib COMMENT 'updated' --- case -ALTER LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' ENGINE = NDB --- case -ALTER PROCEDURE myproc COMMENT 'some comment' --- case -ALTER SERVER s OPTIONS (USER 'user') --- case -ALTER TABLESPACE ts ADD DATAFILE 'file.ibd' ENGINE = NDB --- case -ALTER VIEW v AS SELECT 1 --- case -CREATE EVENT e ON SCHEDULE AT CURRENT_TIMESTAMP DO SELECT 1 --- case -CREATE FUNCTION f(x INT) RETURNS INT DETERMINISTIC RETURN x + 1 --- case -CREATE JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id) FROM t --- case -CREATE LIBRARY mylib LANGUAGE JAVASCRIPT AS 'export function f() { return 1 }' --- case -CREATE LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' ENGINE = NDB --- case -CREATE MASKING POLICY p ON t (c) USING (mask_inner(c, 1, 1)) --- case -CREATE SERVER s FOREIGN DATA WRAPPER mysql OPTIONS (USER 'user', HOST 'host', DATABASE 'db') --- case -CREATE SPATIAL REFERENCE SYSTEM 5000 NAME 'my srs' DEFINITION 'GEOGCS[]' --- case -CREATE TABLESPACE ts ADD DATAFILE 'file.ibd' ENGINE = INNODB --- case -CREATE TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET @x = 1 --- case -DROP EVENT e --- case -DROP FUNCTION f --- case -DROP LIBRARY mylib --- case -DROP LOGFILE GROUP lg1 ENGINE = NDB --- case -DROP MASKING POLICY p --- case -DROP SERVER s --- case -DROP SPATIAL REFERENCE SYSTEM 5000 --- case -DROP TABLESPACE ts ENGINE = INNODB --- case -DROP TRIGGER trg diff --git a/parser/testdata/parser/mysql_unsupported_ddl/output.sql b/parser/testdata/parser/mysql_unsupported_ddl/output.sql deleted file mode 100644 index c4a8ce5..0000000 --- a/parser/testdata/parser/mysql_unsupported_ddl/output.sql +++ /dev/null @@ -1,57 +0,0 @@ --- error: line 1 column 5 near "ALTER EVENT myevent ON SCHEDULE EVERY 2 HOUR" --- case --- error: line 1 column 5 near "ALTER FUNCTION myfunc COMMENT 'some comment'" --- case --- error: line 1 column 21 near "ROTATE INNODB MASTER KEY" --- case --- error: line 1 column 5 near "ALTER JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id) FROM t" --- case --- error: line 1 column 5 near "ALTER LIBRARY mylib COMMENT 'updated'" --- case --- error: line 1 column 5 near "ALTER LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' ENGINE = NDB" --- case --- error: line 1 column 5 near "ALTER PROCEDURE myproc COMMENT 'some comment'" --- case --- error: line 1 column 5 near "ALTER SERVER s OPTIONS (USER 'user')" --- case --- error: line 1 column 5 near "ALTER TABLESPACE ts ADD DATAFILE 'file.ibd' ENGINE = NDB" --- case --- error: line 1 column 5 near "ALTER VIEW v AS SELECT 1" --- case --- error: line 1 column 12 near "EVENT e ON SCHEDULE AT CURRENT_TIMESTAMP DO SELECT 1" --- case --- error: line 1 column 18 near "(x INT) RETURNS INT DETERMINISTIC RETURN x + 1" --- case --- error: line 1 column 11 near "JSON DUALITY VIEW jdv AS SELECT JSON_DUALITY_OBJECT('id' : t.id) FROM t" --- case --- error: line 1 column 14 near "LIBRARY mylib LANGUAGE JAVASCRIPT AS 'export function f() { return 1 }'" --- case --- error: line 1 column 14 near "LOGFILE GROUP lg1 ADD UNDOFILE 'undo.dat' ENGINE = NDB" --- case --- error: line 1 column 38 near "USING (mask_inner(c, 1, 1))" --- case --- error: line 1 column 13 near "SERVER s FOREIGN DATA WRAPPER mysql OPTIONS (USER 'user', HOST 'host', DATABASE 'db')" --- case --- error: line 1 column 24 near "REFERENCE SYSTEM 5000 NAME 'my srs' DEFINITION 'GEOGCS[]'" --- case --- error: line 1 column 17 near "TABLESPACE ts ADD DATAFILE 'file.ibd' ENGINE = INNODB" --- case --- error: line 1 column 14 near "TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET @x = 1" --- case --- error: line 1 column 4 near "DROP EVENT e" --- case --- error: line 1 column 4 near "DROP FUNCTION f" --- case --- error: line 1 column 4 near "DROP LIBRARY mylib" --- case --- error: line 1 column 4 near "DROP LOGFILE GROUP lg1 ENGINE = NDB" --- case --- error: line 1 column 4 near "DROP MASKING POLICY p" --- case --- error: line 1 column 4 near "DROP SERVER s" --- case --- error: line 1 column 4 near "DROP SPATIAL REFERENCE SYSTEM 5000" --- case --- error: line 1 column 4 near "DROP TABLESPACE ts ENGINE = INNODB" --- case --- error: line 1 column 4 near "DROP TRIGGER trg" diff --git a/parser/token_kinds.go b/parser/token_kinds.go index 2d0e877..df0ff3a 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -81,6 +81,7 @@ const ( as = 57369 asc = 57370 ascii = 57608 + at = 58296 asof = 57347 assignmentEq = 58207 attribute = 57609 @@ -198,6 +199,7 @@ const ( commit = 57657 committed = 57658 compact = 57659 + completion = 58297 component = 58261 compress = 58007 compressed = 57660 @@ -212,6 +214,7 @@ const ( consistent = 57668 constraint = 57386 constraints = 58008 + contains = 58298 context = 57669 continueKwd = 57387 convert = 57388 @@ -246,6 +249,7 @@ const ( databases = 57399 dateAdd = 58013 dateSub = 58014 + datafile = 58299 dateType = 57681 datetimeType = 57682 day = 57683 @@ -269,6 +273,7 @@ const ( depth = 58163 desc = 57409 describe = 57410 + deterministic = 58300 diagnostics = 58253 digest = 57688 directory = 57689 @@ -290,11 +295,13 @@ const ( dry = 58167 dryRun = 58017 dual = 57416 + duality = 58301 dump = 58018 dumpfile = 58293 duplicate = 57695 dynamic = 57696 elseIfKwd = 57418 + each = 58302 elseKwd = 57417 empty = 58220 enable = 57697 @@ -304,6 +311,7 @@ const ( encryptionKeyFile = 57700 encryptionMethod = 57701 end = 57702 + ends = 58303 endTime = 58019 enforced = 57703 engine = 57704 @@ -317,6 +325,7 @@ const ( escaped = 57420 event = 57711 events = 57712 + every = 58304 evolve = 57713 exact = 58020 except = 57421 @@ -355,6 +364,7 @@ const ( followerConstraints = 58026 followers = 58027 following = 57728 + follows = 58305 forKwd = 57431 force = 57432 foreign = 57433 @@ -416,6 +426,7 @@ const ( indexes = 57751 infile = 57450 inner = 57451 + innodb = 58306 inout = 57452 inplace = 58033 insert = 57453 @@ -455,6 +466,7 @@ const ( jsonArrayagg = 58039 jsonObjectAgg = 58040 jsonSumCrc32 = 58041 + jsonDualityObject = 58307 jsonType = 57760 jss = 58211 juss = 58212 @@ -499,6 +511,7 @@ const ( lock = 57484 locked = 57773 log = 58047 + logfile = 58308 logs = 57774 long = 57485 longblobType = 57486 @@ -560,6 +573,7 @@ const ( mod = 57497 mode = 57792 moderated = 58114 + modifies = 58309 modify = 57793 monitor = 57794 month = 57795 @@ -619,6 +633,7 @@ const ( option = 57508 optional = 57822 optionally = 57509 + options = 58310 optionallyEnclosedBy = 57351 or = 57510 order = 57511 @@ -661,6 +676,7 @@ const ( policy = 57841 position = 58060 preSplitRegions = 57845 + precedes = 58311 preceding = 57842 precisionType = 57518 predicate = 58061 @@ -690,6 +706,7 @@ const ( rateLimit = 57857 raw = 58178 read = 57523 + reads = 58312 readOnly = 58066 realType = 57524 rebuild = 57858 @@ -730,6 +747,7 @@ const ( restores = 57876 restrict = 57533 resume = 57877 + returnKwd = 58313 returns = 58267 reuse = 57878 reverse = 57879 @@ -739,6 +757,7 @@ const ( role = 57880 rollback = 57881 rollup = 57882 + rotate = 58314 routine = 57883 row = 57537 rowCount = 57884 @@ -772,6 +791,7 @@ const ( sequence = 57899 serial = 57900 serializable = 57901 + server = 58315 session = 57902 sessionStates = 58185 set = 57542 @@ -824,6 +844,7 @@ const ( startTS = 58080 startTime = 58079 starting = 57554 + starts = 58316 statistics = 58187 stats = 58188 statsAutoRecalc = 57929 @@ -937,6 +958,8 @@ const ( uncommitted = 57973 undefined = 57974 underscoreCS = 57352 + undo = 58317 + undofile = 58318 unicodeSym = 57975 uninstall = 58271 union = 57569 @@ -994,6 +1017,7 @@ const ( withSysTable = 57991 without = 57990 workload = 57992 + wrapper = 58319 write = 57592 x509 = 57993 xa = 58284