diff --git a/ast/sem.go b/ast/sem.go index 2370978..c202b52 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -468,6 +468,12 @@ const ( // ProcedureCommand represents all statements in procedure. It's too rough // but still fine for now. ProcedureCommand = "PROCEDURE" + // SignalCommand represents SIGNAL statement + SignalCommand = "SIGNAL" + // ResignalCommand represents RESIGNAL statement + ResignalCommand = "RESIGNAL" + // GetDiagnosticsCommand represents GET DIAGNOSTICS statement + GetDiagnosticsCommand = "GET DIAGNOSTICS" // UnknownCommand represents unknown statements UnknownCommand = "UNKNOWN" // SetOprCommand represents UNION/INTERSECT/EXCEPT statement @@ -1341,3 +1347,18 @@ func (n *ProcedureErrorVal) SEMCommand() string { func (n *ProcedureErrorState) SEMCommand() string { return ProcedureCommand } + +// SEMCommand returns the command string for the statement. +func (n *SignalStmt) SEMCommand() string { + return SignalCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ResignalStmt) SEMCommand() string { + return ResignalCommand +} + +// SEMCommand returns the command string for the statement. +func (n *GetDiagnosticsStmt) SEMCommand() string { + return GetDiagnosticsCommand +} diff --git a/ast/signal.go b/ast/signal.go new file mode 100644 index 0000000..3a8ebeb --- /dev/null +++ b/ast/signal.go @@ -0,0 +1,309 @@ +// 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" +) + +// SIGNAL, RESIGNAL, and GET DIAGNOSTICS: the condition handling +// statements of MySQL's compound statement syntax. + +var ( + _ Node = &SignalConditionValue{} + _ Node = &SignalSetItem{} + _ Node = &DiagnosticsItem{} + + _ StmtNode = &SignalStmt{} + _ StmtNode = &ResignalStmt{} + _ StmtNode = &GetDiagnosticsStmt{} +) + +// SignalConditionValue is the condition_value of a SIGNAL or RESIGNAL +// statement: either SQLSTATE [VALUE] 'xxxxx' or the name of a condition +// declared with DECLARE ... CONDITION. Exactly one of SQLState and +// ConditionName is set. +type SignalConditionValue struct { + node + SQLState string + ConditionName string +} + +// Restore implements Node interface. +func (n *SignalConditionValue) Restore(ctx *format.RestoreCtx) error { + if n.SQLState != "" { + ctx.WriteKeyWord("SQLSTATE ") + ctx.WriteString(n.SQLState) + } else { + ctx.WriteName(n.ConditionName) + } + return nil +} + +// Accept implements Node Accept interface. +func (n *SignalConditionValue) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*SignalConditionValue) + return v.Leave(n) +} + +// SignalSetItem is one signal_information_item of a SIGNAL or RESIGNAL +// SET clause: a condition information item name and its value. Name is +// the canonical uppercase item name (e.g. "MESSAGE_TEXT"); Value is a +// literal or a variable. +type SignalSetItem struct { + node + Name string + Value ExprNode +} + +// Restore implements Node interface. +func (n *SignalSetItem) 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 SignalSetItem.Value") + } + return nil +} + +// Accept implements Node Accept interface. +func (n *SignalSetItem) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*SignalSetItem) + node, ok := n.Value.Accept(v) + if !ok { + return n, false + } + n.Value = node.(ExprNode) + return v.Leave(n) +} + +// SignalStmt is a SIGNAL statement: +// SIGNAL condition_value [SET signal_information_item, ...]. +type SignalStmt struct { + stmtNode + Condition *SignalConditionValue + SetItems []*SignalSetItem +} + +// Restore implements Node interface. +func (n *SignalStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("SIGNAL ") + if err := n.Condition.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore SignalStmt.Condition") + } + return restoreSignalSetItems(ctx, n.SetItems) +} + +// Accept implements Node Accept interface. +func (n *SignalStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*SignalStmt) + node, ok := n.Condition.Accept(v) + if !ok { + return n, false + } + n.Condition = node.(*SignalConditionValue) + for i, item := range n.SetItems { + node, ok := item.Accept(v) + if !ok { + return n, false + } + n.SetItems[i] = node.(*SignalSetItem) + } + return v.Leave(n) +} + +// ResignalStmt is a RESIGNAL statement: +// RESIGNAL [condition_value] [SET signal_information_item, ...]. +// Condition is nil when no condition value is given. +type ResignalStmt struct { + stmtNode + Condition *SignalConditionValue + SetItems []*SignalSetItem +} + +// Restore implements Node interface. +func (n *ResignalStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("RESIGNAL") + if n.Condition != nil { + ctx.WritePlain(" ") + if err := n.Condition.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ResignalStmt.Condition") + } + } + return restoreSignalSetItems(ctx, n.SetItems) +} + +// Accept implements Node Accept interface. +func (n *ResignalStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ResignalStmt) + if n.Condition != nil { + node, ok := n.Condition.Accept(v) + if !ok { + return n, false + } + n.Condition = node.(*SignalConditionValue) + } + for i, item := range n.SetItems { + node, ok := item.Accept(v) + if !ok { + return n, false + } + n.SetItems[i] = node.(*SignalSetItem) + } + return v.Leave(n) +} + +func restoreSignalSetItems(ctx *format.RestoreCtx, items []*SignalSetItem) error { + for i, item := range items { + if i == 0 { + ctx.WriteKeyWord(" SET ") + } else { + ctx.WritePlain(", ") + } + if err := item.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore SignalSetItem") + } + } + return nil +} + +// DiagnosticsArea selects which diagnostics area a GET DIAGNOSTICS +// statement reads. +type DiagnosticsArea int + +const ( + // DiagnosticsAreaDefault omits the area keyword (the current area). + DiagnosticsAreaDefault DiagnosticsArea = iota + // DiagnosticsAreaCurrent is GET CURRENT DIAGNOSTICS. + DiagnosticsAreaCurrent + // DiagnosticsAreaStacked is GET STACKED DIAGNOSTICS. + DiagnosticsAreaStacked +) + +// DiagnosticsItem is one target = item_name assignment of a GET +// DIAGNOSTICS statement. Target is a user variable or a stored program +// local variable; Name is the canonical uppercase statement or condition +// information item name (e.g. "ROW_COUNT", "RETURNED_SQLSTATE"). +type DiagnosticsItem struct { + node + Target ExprNode + Name string +} + +// Restore implements Node interface. +func (n *DiagnosticsItem) Restore(ctx *format.RestoreCtx) error { + if err := n.Target.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore DiagnosticsItem.Target") + } + ctx.WritePlain("=") + ctx.WriteKeyWord(n.Name) + return nil +} + +// Accept implements Node Accept interface. +func (n *DiagnosticsItem) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*DiagnosticsItem) + node, ok := n.Target.Accept(v) + if !ok { + return n, false + } + n.Target = node.(ExprNode) + return v.Leave(n) +} + +// GetDiagnosticsStmt is a GET DIAGNOSTICS statement: +// +// GET [CURRENT | STACKED] DIAGNOSTICS +// { statement_information_item [, ...] +// | CONDITION condition_number condition_information_item [, ...] } +// +// ConditionNumber is non-nil for the CONDITION form. +type GetDiagnosticsStmt struct { + stmtNode + Area DiagnosticsArea + ConditionNumber ExprNode + Items []*DiagnosticsItem +} + +// Restore implements Node interface. +func (n *GetDiagnosticsStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("GET ") + switch n.Area { + case DiagnosticsAreaCurrent: + ctx.WriteKeyWord("CURRENT ") + case DiagnosticsAreaStacked: + ctx.WriteKeyWord("STACKED ") + } + ctx.WriteKeyWord("DIAGNOSTICS ") + if n.ConditionNumber != nil { + ctx.WriteKeyWord("CONDITION ") + if err := n.ConditionNumber.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore GetDiagnosticsStmt.ConditionNumber") + } + ctx.WritePlain(" ") + } + for i, item := range n.Items { + if i != 0 { + ctx.WritePlain(", ") + } + if err := item.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore DiagnosticsItem") + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *GetDiagnosticsStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*GetDiagnosticsStmt) + if n.ConditionNumber != nil { + node, ok := n.ConditionNumber.Accept(v) + if !ok { + return n, false + } + n.ConditionNumber = node.(ExprNode) + } + for i, item := range n.Items { + node, ok := item.Accept(v) + if !ok { + return n, false + } + n.Items[i] = node.(*DiagnosticsItem) + } + return v.Leave(n) +} diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index b241ea6..f8afbcd 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -411,6 +411,8 @@ var unReservedKeywordNames = []string{ "PAGE_COMPRESSION_LEVEL", "TRANSACTIONAL", "IETF_QUOTES", + "DIAGNOSTICS", + "STACKED", } // notKeywordTokenNames lists the NotKeywordToken production alternatives of parser.y. diff --git a/parser/keywords.go b/parser/keywords.go index 3744a8d..b3f321d 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -102,6 +102,7 @@ var Keywords = []KeywordsType{ {"FROM", true, "reserved"}, {"FULLTEXT", true, "reserved"}, {"GENERATED", true, "reserved"}, + {"GET", true, "reserved"}, {"GRANT", true, "reserved"}, {"GROUP", true, "reserved"}, {"GROUPS", true, "reserved"}, @@ -199,6 +200,7 @@ var Keywords = []KeywordsType{ {"REPEAT", true, "reserved"}, {"REPLACE", true, "reserved"}, {"REQUIRE", true, "reserved"}, + {"RESIGNAL", true, "reserved"}, {"RESTRICT", true, "reserved"}, {"REVOKE", true, "reserved"}, {"RIGHT", true, "reserved"}, @@ -210,6 +212,7 @@ var Keywords = []KeywordsType{ {"SELECT", true, "reserved"}, {"SET", true, "reserved"}, {"SHOW", true, "reserved"}, + {"SIGNAL", true, "reserved"}, {"SMALLINT", true, "reserved"}, {"SPATIAL", true, "reserved"}, {"SQL", true, "reserved"}, @@ -355,6 +358,7 @@ var Keywords = []KeywordsType{ {"DECLARE", false, "unreserved"}, {"DEFINER", false, "unreserved"}, {"DELAY_KEY_WRITE", false, "unreserved"}, + {"DIAGNOSTICS", false, "unreserved"}, {"DIGEST", false, "unreserved"}, {"DIRECTORY", false, "unreserved"}, {"DISABLE", false, "unreserved"}, @@ -595,6 +599,7 @@ var Keywords = []KeywordsType{ {"SQL_TSI_SECOND", false, "unreserved"}, {"SQL_TSI_WEEK", false, "unreserved"}, {"SQL_TSI_YEAR", false, "unreserved"}, + {"STACKED", false, "unreserved"}, {"START", false, "unreserved"}, {"STATS_AUTO_RECALC", false, "unreserved"}, {"STATS_COL_CHOICE", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 08a6f0a..2909de6 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(685, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 685) + if !reflect.DeepEqual(690, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 690) } reservedNr := 0 @@ -53,8 +53,8 @@ func TestKeywordsLength(t *testing.T) { reservedNr += 1 } } - if !reflect.DeepEqual(236, reservedNr) { - t.Fatalf("got %v, want %v", reservedNr, 236) + if !reflect.DeepEqual(239, reservedNr) { + t.Fatalf("got %v, want %v", reservedNr, 239) } } diff --git a/parser/misc.go b/parser/misc.go index 0d81164..71b8c8f 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -340,6 +340,7 @@ var tokenMap = map[string]int{ "DEPTH": depth, "DESC": desc, "DESCRIBE": describe, + "DIAGNOSTICS": diagnostics, "DIGEST": digest, "DIRECTORY": directory, "DISABLE": disable, @@ -429,6 +430,7 @@ var tokenMap = map[string]int{ "GC_TTL": gcTTL, "GENERAL": general, "GENERATED": generated, + "GET": get, "GET_FORMAT": getFormat, "GLOBAL": global, "GRANT": grant, @@ -708,6 +710,7 @@ var tokenMap = map[string]int{ "REQUIRE": require, "REQUIRED": required, "RESET": reset, + "RESIGNAL": resignal, "RESOURCE": resource, "RESPECT": respect, "RESTART": restart, @@ -763,6 +766,7 @@ var tokenMap = map[string]int{ "SHARED": shared, "SHOW": show, "SHUTDOWN": shutdown, + "SIGNAL": signal, "SIGNED": signed, "SIMILAR": similar, "SIMPLE": simple, @@ -796,6 +800,7 @@ var tokenMap = map[string]int{ "SQLSTATE": sqlstate, "SQLWARNING": sqlwarning, "SSL": ssl, + "STACKED": stacked, "STALENESS": staleness, "START": start, "START_TIME": startTime, diff --git a/parser/parse_procedure.go b/parser/parse_procedure.go index c5efda3..9ae1676 100644 --- a/parser/parse_procedure.go +++ b/parser/parse_procedure.go @@ -205,7 +205,8 @@ func (r *rdParser) parseProcedureProcStmt() ast.StmtNode { // SelectStmt | SelectStmtWithClause | SubSelect | SetStmt | UpdateStmt // | UseStmt | InsertIntoStmt | ReplaceIntoStmt | CommitStmt // | RollbackStmt | ExplainStmt | SetOprStmt | DeleteFromStmt -// | AnalyzeTableStmt | TruncateTableStmt +// | AnalyzeTableStmt | TruncateTableStmt | SignalStmt | ResignalStmt +// | GetDiagnosticsStmt // // The SubSelect alternative carries the usual IsInBraces action, which // finishSelectFamily reproduces. @@ -247,6 +248,12 @@ func (r *rdParser) parseProcedureStatementStmt() ast.StmtNode { return r.parseAnalyzeTableStmt() case truncate: return r.parseTruncateTableStmt() + case signal: + return r.parseSignalStmt() + case resignal: + return r.parseResignalStmt() + case get: + return r.parseGetDiagnosticsStmt() } r.syntaxError() return nil diff --git a/parser/parse_signal.go b/parser/parse_signal.go new file mode 100644 index 0000000..daf5de4 --- /dev/null +++ b/parser/parse_signal.go @@ -0,0 +1,214 @@ +// 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 + +// Condition handling statements: SIGNAL, RESIGNAL, and GET DIAGNOSTICS. +// These postdate the goyacc grammar; the productions below are written +// from the MySQL 26.7 reference manual (ยง15.6.7, Condition Handling) +// in the same style as parser.y. + +import ( + "strings" + + "github.com/sqlc-dev/marino/ast" +) + +func init() { + rdRegister(signal, (*rdParser).parseSignalStmt) + rdRegister(resignal, (*rdParser).parseResignalStmt) + rdRegister(get, (*rdParser).parseGetDiagnosticsStmt) +} + +// signalInfoItemNames are the condition information item names a SIGNAL +// or RESIGNAL SET clause may assign. GET DIAGNOSTICS ... CONDITION reads +// the same items plus RETURNED_SQLSTATE (conditionInfoItemNames), and the +// statement form reads statementInfoItemNames. The names are not keywords +// (they are non-reserved in MySQL); they are matched case-insensitively +// in identifier position. +var signalInfoItemNames = map[string]bool{ + "CLASS_ORIGIN": true, + "SUBCLASS_ORIGIN": true, + "MESSAGE_TEXT": true, + "MYSQL_ERRNO": true, + "CONSTRAINT_CATALOG": true, + "CONSTRAINT_SCHEMA": true, + "CONSTRAINT_NAME": true, + "CATALOG_NAME": true, + "SCHEMA_NAME": true, + "TABLE_NAME": true, + "COLUMN_NAME": true, + "CURSOR_NAME": true, +} + +var statementInfoItemNames = map[string]bool{ + "NUMBER": true, + "ROW_COUNT": true, +} + +var conditionInfoItemNames = func() map[string]bool { + m := map[string]bool{"RETURNED_SQLSTATE": true} + for name := range signalInfoItemNames { + m[name] = true + } + return m +}() + +// parseSignalStmt implements SignalStmt: +// "SIGNAL" SignalConditionValue SignalSetOpt. +func (r *rdParser) parseSignalStmt() ast.StmtNode { + r.expect(signal) + return &ast.SignalStmt{ + Condition: r.parseSignalConditionValue(), + SetItems: r.parseSignalSetOpt(), + } +} + +// parseResignalStmt implements ResignalStmt: +// "RESIGNAL" SignalConditionValueOpt SignalSetOpt. +func (r *rdParser) parseResignalStmt() ast.StmtNode { + r.expect(resignal) + x := &ast.ResignalStmt{} + if r.tok() == sqlstate || isIdentifierTok(r.tok()) { + x.Condition = r.parseSignalConditionValue() + } + x.SetItems = r.parseSignalSetOpt() + return x +} + +// parseSignalConditionValue implements SignalConditionValue: +// +// "SQLSTATE" optValue stringLit | Identifier +// +// The identifier alternative names a condition declared with +// DECLARE ... CONDITION. +func (r *rdParser) parseSignalConditionValue() *ast.SignalConditionValue { + if r.tok() == sqlstate { + r.advance() + // optValue: empty | "VALUE" + r.accept(value) + return &ast.SignalConditionValue{SQLState: r.expect(stringLit).lit} + } + return &ast.SignalConditionValue{ConditionName: r.parseIdentifier()} +} + +// parseSignalSetOpt implements SignalSetOpt: +// empty | "SET" SignalSetItem (',' SignalSetItem)*. +func (r *rdParser) parseSignalSetOpt() []*ast.SignalSetItem { + if !r.accept(set) { + return nil + } + items := []*ast.SignalSetItem{r.parseSignalSetItem()} + for r.accept(int(',')) { + items = append(items, r.parseSignalSetItem()) + } + return items +} + +// parseSignalSetItem implements SignalSetItem (signal_information_item): +// SignalInfoItemName eq SignalAllowedExpr. +func (r *rdParser) parseSignalSetItem() *ast.SignalSetItem { + name := r.parseInfoItemName(signalInfoItemNames) + r.expect(eq) + return &ast.SignalSetItem{Name: name, Value: r.parseSignalAllowedExpr()} +} + +// parseInfoItemName consumes an Identifier and validates it against the +// allowed information item names of the enclosing production, returning +// the canonical uppercase spelling. +func (r *rdParser) parseInfoItemName(allowed map[string]bool) string { + if !isIdentifierTok(r.tok()) { + r.syntaxError() + } + name := strings.ToUpper(r.cur().lit) + if !allowed[name] { + r.syntaxError() + } + r.advance() + return name +} + +// parseSignalAllowedExpr implements SignalAllowedExpr (the manual's +// simple_value_specification): Literal | UserVariable | SystemVariable +// | Identifier, the identifier being a stored program variable. +func (r *rdParser) parseSignalAllowedExpr() ast.ExprNode { + start := r.cur().offset + switch { + case r.tok() == singleAtIdentifier: + return r.parseUserVariable() + case r.tok() == doubleAtIdentifier: + return r.parseSystemVariable(start) + case isIdentifierTok(r.tok()): + name := &ast.ColumnName{Name: ast.NewCIStr(r.parseIdentifier())} + return r.setOrigin(&ast.ColumnNameExpr{Name: name}, start) + } + return r.parseLiteralExpr(start) +} + +// parseGetDiagnosticsStmt implements GetDiagnosticsStmt: +// +// "GET" OptDiagnosticsArea "DIAGNOSTICS" ( +// DiagnosticsItem (',' DiagnosticsItem)* +// | "CONDITION" SignalAllowedExpr DiagnosticsItem (',' DiagnosticsItem)* ) +// +// with OptDiagnosticsArea: empty | "CURRENT" | "STACKED". The statement +// form assigns statementInfoItemNames, the CONDITION form +// conditionInfoItemNames. CONDITION is reserved in MySQL but not a +// keyword here, so it is matched by spelling; a diagnostics target can +// therefore not be named "condition", exactly as in MySQL. +func (r *rdParser) parseGetDiagnosticsStmt() ast.StmtNode { + r.expect(get) + x := &ast.GetDiagnosticsStmt{} + switch r.tok() { + case current: + r.advance() + x.Area = ast.DiagnosticsAreaCurrent + case stacked: + r.advance() + x.Area = ast.DiagnosticsAreaStacked + } + r.expect(diagnostics) + names := statementInfoItemNames + if isIdentifierTok(r.tok()) && strings.EqualFold(r.cur().lit, "CONDITION") { + r.advance() + x.ConditionNumber = r.parseSignalAllowedExpr() + names = conditionInfoItemNames + } + x.Items = []*ast.DiagnosticsItem{r.parseDiagnosticsItem(names)} + for r.accept(int(',')) { + x.Items = append(x.Items, r.parseDiagnosticsItem(names)) + } + return x +} + +// parseDiagnosticsItem implements DiagnosticsItem +// (statement_information_item / condition_information_item): +// DiagnosticsTarget eq InfoItemName. +func (r *rdParser) parseDiagnosticsItem(allowed map[string]bool) *ast.DiagnosticsItem { + item := &ast.DiagnosticsItem{Target: r.parseDiagnosticsTarget()} + r.expect(eq) + item.Name = r.parseInfoItemName(allowed) + return item +} + +// parseDiagnosticsTarget implements DiagnosticsTarget (the manual's +// simple_target_specification): UserVariable | Identifier, the +// identifier being a stored program variable. +func (r *rdParser) parseDiagnosticsTarget() ast.ExprNode { + start := r.cur().offset + if r.tok() == singleAtIdentifier { + return r.parseUserVariable() + } + name := &ast.ColumnName{Name: ast.NewCIStr(r.parseIdentifier())} + return r.setOrigin(&ast.ColumnNameExpr{Name: name}, start) +} diff --git a/parser/testdata/parser/mysql_compound/input.sql b/parser/testdata/parser/mysql_compound/input.sql new file mode 100644 index 0000000..249945e --- /dev/null +++ b/parser/testdata/parser/mysql_compound/input.sql @@ -0,0 +1,39 @@ +SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'error' +-- case +SIGNAL SQLSTATE VALUE '45000' +-- case +signal sqlstate '01000' set class_origin = 'ISO 9075', mysql_errno = 1000 +-- case +SIGNAL my_condition +-- case +SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = @msg, MYSQL_ERRNO = @errno +-- case +RESIGNAL +-- case +RESIGNAL SQLSTATE '45000' +-- case +RESIGNAL SET MYSQL_ERRNO = 5 +-- case +RESIGNAL some_condition SET MESSAGE_TEXT = 'x' +-- case +GET DIAGNOSTICS @n = NUMBER +-- case +GET DIAGNOSTICS @n = NUMBER, @r = ROW_COUNT +-- case +GET CURRENT DIAGNOSTICS @n = NUMBER +-- case +get stacked diagnostics @n = number +-- case +GET DIAGNOSTICS CONDITION 1 @p1 = RETURNED_SQLSTATE, @p2 = MESSAGE_TEXT +-- case +GET STACKED DIAGNOSTICS CONDITION @cno @errno = MYSQL_ERRNO +-- case +CREATE PROCEDURE `p`() BEGIN DECLARE `msg` TEXT;GET DIAGNOSTICS CONDITION 1 `msg`=MESSAGE_TEXT;RESIGNAL SET MESSAGE_TEXT=`msg`; END +-- case +CREATE PROCEDURE `p`() BEGIN IF @`x`>1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT=_UTF8MB4'too big';END IF; END +-- case +SIGNAL +-- case +SIGNAL SQLSTATE '45000' SET BOGUS_ITEM = 'x' +-- case +GET DIAGNOSTICS @n = MESSAGE_TEXT diff --git a/parser/testdata/parser/mysql_compound/output.sql b/parser/testdata/parser/mysql_compound/output.sql new file mode 100644 index 0000000..5b4fc4b --- /dev/null +++ b/parser/testdata/parser/mysql_compound/output.sql @@ -0,0 +1,39 @@ +SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT=_UTF8MB4'error' +-- case +SIGNAL SQLSTATE '45000' +-- case +SIGNAL SQLSTATE '01000' SET CLASS_ORIGIN=_UTF8MB4'ISO 9075', MYSQL_ERRNO=1000 +-- case +SIGNAL `my_condition` +-- case +SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT=@`msg`, MYSQL_ERRNO=@`errno` +-- case +RESIGNAL +-- case +RESIGNAL SQLSTATE '45000' +-- case +RESIGNAL SET MYSQL_ERRNO=5 +-- case +RESIGNAL `some_condition` SET MESSAGE_TEXT=_UTF8MB4'x' +-- case +GET DIAGNOSTICS @`n`=NUMBER +-- case +GET DIAGNOSTICS @`n`=NUMBER, @`r`=ROW_COUNT +-- case +GET CURRENT DIAGNOSTICS @`n`=NUMBER +-- case +GET STACKED DIAGNOSTICS @`n`=NUMBER +-- case +GET DIAGNOSTICS CONDITION 1 @`p1`=RETURNED_SQLSTATE, @`p2`=MESSAGE_TEXT +-- case +GET STACKED DIAGNOSTICS CONDITION @`cno` @`errno`=MYSQL_ERRNO +-- case +CREATE PROCEDURE `p`() BEGIN DECLARE `msg` TEXT;GET DIAGNOSTICS CONDITION 1 `msg`=MESSAGE_TEXT;RESIGNAL SET MESSAGE_TEXT=`msg`; END +-- case +CREATE PROCEDURE `p`() BEGIN IF @`x`>1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT=_UTF8MB4'too big';END IF; END +-- case +-- error: line 1 column 6 near "" +-- case +-- error: line 1 column 38 near "BOGUS_ITEM = 'x'" +-- case +-- error: line 1 column 33 near "MESSAGE_TEXT" diff --git a/parser/testdata/parser/mysql_unsupported_admin/input.sql b/parser/testdata/parser/mysql_unsupported_admin/input.sql new file mode 100644 index 0000000..175e967 --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_admin/input.sql @@ -0,0 +1,29 @@ +CREATE RESOURCE GROUP rg1 TYPE = USER VCPU = 0-3 +-- case +ALTER RESOURCE GROUP rg1 VCPU = 0-3 +-- case +CHECK TABLE t +-- case +CHECKSUM TABLE t +-- case +REPAIR TABLE t +-- case +CREATE FUNCTION metaphon RETURNS STRING SONAME 'udf.so' +-- case +INSTALL COMPONENT 'file://component_validate_password' +-- case +INSTALL PLUGIN myplugin SONAME 'plugin.so' +-- case +UNINSTALL COMPONENT 'file://component_validate_password' +-- case +UNINSTALL PLUGIN myplugin +-- case +CLONE LOCAL DATA DIRECTORY = '/tmp/clone' +-- case +CLONE INSTANCE FROM 'user'@'host':3306 IDENTIFIED BY 'password' +-- case +CACHE INDEX t IN hot_cache +-- case +LOAD INDEX INTO CACHE t +-- case +RESET PERSIST diff --git a/parser/testdata/parser/mysql_unsupported_admin/output.sql b/parser/testdata/parser/mysql_unsupported_admin/output.sql new file mode 100644 index 0000000..b22145e --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_admin/output.sql @@ -0,0 +1,29 @@ +-- error: line 1 column 30 near "TYPE = USER VCPU = 0-3" +-- case +-- error: line 1 column 29 near "VCPU = 0-3" +-- case +-- error: line 1 column 5 near "CHECK TABLE t" +-- case +-- error: line 1 column 8 near "CHECKSUM TABLE t" +-- case +-- error: line 1 column 6 near "REPAIR TABLE t" +-- case +-- error: line 1 column 15 near "FUNCTION metaphon RETURNS STRING SONAME 'udf.so'" +-- case +-- error: line 1 column 7 near "INSTALL COMPONENT 'file://component_validate_password'" +-- case +-- error: line 1 column 7 near "INSTALL PLUGIN myplugin SONAME 'plugin.so'" +-- case +-- error: line 1 column 9 near "UNINSTALL COMPONENT 'file://component_validate_password'" +-- case +-- error: line 1 column 9 near "UNINSTALL PLUGIN myplugin" +-- case +-- error: line 1 column 5 near "CLONE LOCAL DATA DIRECTORY = '/tmp/clone'" +-- case +-- error: line 1 column 5 near "CLONE INSTANCE FROM 'user'@'host':3306 IDENTIFIED BY 'password'" +-- case +-- error: line 1 column 5 near "CACHE INDEX t IN hot_cache" +-- case +-- error: line 1 column 4 near "LOAD INDEX INTO CACHE t" +-- case +-- error: line 1 column 5 near "RESET PERSIST" diff --git a/parser/testdata/parser/mysql_unsupported_ddl/input.sql b/parser/testdata/parser/mysql_unsupported_ddl/input.sql new file mode 100644 index 0000000..275bd3b --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_ddl/input.sql @@ -0,0 +1,57 @@ +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 new file mode 100644 index 0000000..ffab5ab --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_ddl/output.sql @@ -0,0 +1,57 @@ +-- 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 15 near "FUNCTION f(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/testdata/parser/mysql_unsupported_dml/input.sql b/parser/testdata/parser/mysql_unsupported_dml/input.sql new file mode 100644 index 0000000..32dd239 --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_dml/input.sql @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..f179149 --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_dml/output.sql @@ -0,0 +1,13 @@ +-- 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/testdata/parser/mysql_unsupported_replication/input.sql b/parser/testdata/parser/mysql_unsupported_replication/input.sql new file mode 100644 index 0000000..43a863c --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_replication/input.sql @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..a3c8d9a --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_replication/output.sql @@ -0,0 +1,19 @@ +-- 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/testdata/parser/mysql_unsupported_show/input.sql b/parser/testdata/parser/mysql_unsupported_show/input.sql new file mode 100644 index 0000000..8af11f6 --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_show/input.sql @@ -0,0 +1,27 @@ +SHOW BINARY LOGS +-- case +SHOW BINLOG EVENTS IN 'binlog.000001' +-- case +SHOW CREATE EVENT e +-- case +SHOW CREATE FUNCTION f +-- case +SHOW CREATE LIBRARY mylib +-- case +SHOW CREATE MASKING POLICY p +-- case +SHOW CREATE TRIGGER trg +-- case +SHOW ENGINE INNODB STATUS +-- case +SHOW FUNCTION CODE f +-- case +SHOW LIBRARY STATUS +-- case +SHOW PARSE_TREE SELECT 1 +-- case +SHOW PROCEDURE CODE p +-- case +SHOW RELAYLOG EVENTS +-- case +SHOW REPLICAS diff --git a/parser/testdata/parser/mysql_unsupported_show/output.sql b/parser/testdata/parser/mysql_unsupported_show/output.sql new file mode 100644 index 0000000..2fc047f --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_show/output.sql @@ -0,0 +1,27 @@ +-- error: line 1 column 16 near "LOGS" +-- case +-- error: line 1 column 11 near "BINLOG EVENTS IN 'binlog.000001'" +-- case +-- error: line 1 column 17 near "EVENT e" +-- case +-- error: line 1 column 20 near "FUNCTION f" +-- case +-- error: line 1 column 19 near "LIBRARY mylib" +-- case +-- error: line 1 column 19 near "MASKING POLICY p" +-- case +-- error: line 1 column 19 near "TRIGGER trg" +-- case +-- error: line 1 column 11 near "ENGINE INNODB STATUS" +-- case +-- error: line 1 column 18 near "CODE f" +-- case +-- error: line 1 column 12 near "LIBRARY STATUS" +-- case +-- error: line 1 column 15 near "PARSE_TREE SELECT 1" +-- case +-- error: line 1 column 19 near "CODE p" +-- case +-- error: line 1 column 13 near "RELAYLOG EVENTS" +-- case +-- error: line 1 column 13 near "REPLICAS" diff --git a/parser/testdata/parser/mysql_unsupported_txn/input.sql b/parser/testdata/parser/mysql_unsupported_txn/input.sql new file mode 100644 index 0000000..5c444ad --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_txn/input.sql @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..ab95b66 --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_txn/output.sql @@ -0,0 +1,17 @@ +-- 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 5aa181b..741dd10 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -262,6 +262,7 @@ const ( depth = 58163 desc = 57409 describe = 57410 + diagnostics = 58253 digest = 57688 directory = 57689 disable = 57690 @@ -358,6 +359,7 @@ const ( ge = 58209 general = 57733 generated = 57436 + get = 58254 getFormat = 58030 global = 57734 grant = 57437 @@ -694,6 +696,7 @@ const ( require = 57532 required = 57871 reset = 58181 + resignal = 58255 resource = 57872 respect = 57873 restart = 57874 @@ -752,6 +755,7 @@ const ( shared = 57906 show = 57543 shutdown = 57907 + signal = 58256 signed = 57908 similar = 58076 simple = 57909 @@ -786,6 +790,7 @@ const ( sqlstate = 57548 sqlwarning = 57549 ssl = 57553 + stacked = 58257 staleness = 58078 start = 57928 startTS = 58080