From 08e24f9d6225524ec6fd7726d5fe8e6c4251b9a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 19:29:23 +0000 Subject: [PATCH 1/2] Add file-driven test cases for MySQL 26.7 statements marino does not parse Probe every statement family documented in the MySQL 26.7 reference manual (Chapter 15, SQL Statements) against the parser and record the gaps as error goldens in seven new testdata/parser groups, organized by manual chapter: - mysql_unsupported_ddl: EVENT, stored/loadable FUNCTION, JSON DUALITY VIEW, LIBRARY, LOGFILE GROUP, PROCEDURE (ALTER), SERVER, TABLESPACE, TRIGGER, SPATIAL REFERENCE SYSTEM, MASKING POLICY, ALTER VIEW, and ALTER INSTANCE ROTATE INNODB MASTER KEY - mysql_unsupported_dml: HANDLER, IMPORT TABLE, LOAD XML, and SELECT ... INTO var - mysql_unsupported_txn: XA transactions and LOCK/UNLOCK INSTANCE - mysql_unsupported_replication: PURGE BINARY LOGS, RESET BINARY LOGS AND GTIDS, CHANGE REPLICATION FILTER, RESET/START/STOP REPLICA, and START/STOP GROUP_REPLICATION - mysql_unsupported_compound: SIGNAL, RESIGNAL, GET DIAGNOSTICS - mysql_unsupported_admin: MySQL-syntax resource groups, CHECK/CHECKSUM/ REPAIR TABLE, INSTALL/UNINSTALL COMPONENT and PLUGIN, CLONE, CACHE INDEX, LOAD INDEX INTO CACHE, RESET PERSIST - mysql_unsupported_show: the SHOW variants that do not parse (BINARY LOGS, BINLOG/RELAYLOG EVENTS, CREATE EVENT/FUNCTION/TRIGGER/LIBRARY/ MASKING POLICY, ENGINE, FUNCTION/PROCEDURE CODE, LIBRARY STATUS, PARSE_TREE, REPLICAS) The goldens document current behavior: when one of these statements gains support, its -- error segment turns into a Restore() golden and the diff makes the coverage change explicit. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XbrFvXrQbj3sTh7SiaUxr7 --- .../parser/mysql_unsupported_admin/input.sql | 29 ++++++++++ .../parser/mysql_unsupported_admin/output.sql | 29 ++++++++++ .../mysql_unsupported_compound/input.sql | 5 ++ .../mysql_unsupported_compound/output.sql | 5 ++ .../parser/mysql_unsupported_ddl/input.sql | 57 +++++++++++++++++++ .../parser/mysql_unsupported_ddl/output.sql | 57 +++++++++++++++++++ .../parser/mysql_unsupported_dml/input.sql | 13 +++++ .../parser/mysql_unsupported_dml/output.sql | 13 +++++ .../mysql_unsupported_replication/input.sql | 19 +++++++ .../mysql_unsupported_replication/output.sql | 19 +++++++ .../parser/mysql_unsupported_show/input.sql | 27 +++++++++ .../parser/mysql_unsupported_show/output.sql | 27 +++++++++ .../parser/mysql_unsupported_txn/input.sql | 17 ++++++ .../parser/mysql_unsupported_txn/output.sql | 17 ++++++ 14 files changed, 334 insertions(+) create mode 100644 parser/testdata/parser/mysql_unsupported_admin/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_admin/output.sql create mode 100644 parser/testdata/parser/mysql_unsupported_compound/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_compound/output.sql create mode 100644 parser/testdata/parser/mysql_unsupported_ddl/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_ddl/output.sql create mode 100644 parser/testdata/parser/mysql_unsupported_dml/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_dml/output.sql create mode 100644 parser/testdata/parser/mysql_unsupported_replication/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_replication/output.sql create mode 100644 parser/testdata/parser/mysql_unsupported_show/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_show/output.sql create mode 100644 parser/testdata/parser/mysql_unsupported_txn/input.sql create mode 100644 parser/testdata/parser/mysql_unsupported_txn/output.sql 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_compound/input.sql b/parser/testdata/parser/mysql_unsupported_compound/input.sql new file mode 100644 index 0000000..eda8c61 --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_compound/input.sql @@ -0,0 +1,5 @@ +SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'error' +-- case +RESIGNAL +-- case +GET DIAGNOSTICS @n = NUMBER diff --git a/parser/testdata/parser/mysql_unsupported_compound/output.sql b/parser/testdata/parser/mysql_unsupported_compound/output.sql new file mode 100644 index 0000000..140ba5b --- /dev/null +++ b/parser/testdata/parser/mysql_unsupported_compound/output.sql @@ -0,0 +1,5 @@ +-- error: line 1 column 6 near "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'error'" +-- case +-- error: line 1 column 8 near "RESIGNAL" +-- case +-- error: line 1 column 3 near "GET DIAGNOSTICS @n = NUMBER" 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" From be55609e402e10598a7471c43f6c7ff5a639bbf0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 19:46:33 +0000 Subject: [PATCH 2/2] Support SIGNAL, RESIGNAL, and GET DIAGNOSTICS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the condition handling statements of MySQL 26.7 §15.6.7, both as top-level statements and inside stored procedure bodies (ProcedureStatementStmt), and rename the mysql_unsupported_compound test group to mysql_compound with expanded coverage now that its statements parse. Grammar, following the reference manual since these postdate the goyacc grammar (parser/parse_signal.go): - SIGNAL condition_value [SET item, ...] and RESIGNAL with both parts optional, where condition_value is SQLSTATE [VALUE] 'xxxxx' or a named condition, and each item assigns a condition information item name a literal or variable value. - GET [CURRENT | STACKED] DIAGNOSTICS with the statement information form (NUMBER, ROW_COUNT) and the CONDITION n form (condition information items plus RETURNED_SQLSTATE); targets are user variables or stored program variables. New AST nodes (ast/signal.go): SignalStmt, ResignalStmt, GetDiagnosticsStmt with SignalConditionValue, SignalSetItem, DiagnosticsItem, and DiagnosticsArea, plus SEMCommand strings. Keyword tables: SIGNAL, RESIGNAL, and GET become reserved words and DIAGNOSTICS and STACKED unreserved, matching their MySQL 26.7 classification; TestKeywordsLength counts updated accordingly. The information item names are not keywords (non-reserved in MySQL) and are matched case-insensitively in identifier position, as is CONDITION. The two in-procedure test cases are written in canonical Restore() spelling: ProcedureBlock.Accept deliberately does not traverse the body statements, so the round-trip check can only normalize node text and expression offsets when input and restored output coincide byte for byte. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XbrFvXrQbj3sTh7SiaUxr7 --- ast/sem.go | 21 ++ ast/signal.go | 309 ++++++++++++++++++ parser/keyword_classes.go | 2 + parser/keywords.go | 5 + parser/keywords_test.go | 8 +- parser/misc.go | 5 + parser/parse_procedure.go | 9 +- parser/parse_signal.go | 214 ++++++++++++ .../testdata/parser/mysql_compound/input.sql | 39 +++ .../testdata/parser/mysql_compound/output.sql | 39 +++ .../mysql_unsupported_compound/input.sql | 5 - .../mysql_unsupported_compound/output.sql | 5 - parser/token_kinds.go | 5 + 13 files changed, 651 insertions(+), 15 deletions(-) create mode 100644 ast/signal.go create mode 100644 parser/parse_signal.go create mode 100644 parser/testdata/parser/mysql_compound/input.sql create mode 100644 parser/testdata/parser/mysql_compound/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_compound/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_compound/output.sql 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_compound/input.sql b/parser/testdata/parser/mysql_unsupported_compound/input.sql deleted file mode 100644 index eda8c61..0000000 --- a/parser/testdata/parser/mysql_unsupported_compound/input.sql +++ /dev/null @@ -1,5 +0,0 @@ -SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'error' --- case -RESIGNAL --- case -GET DIAGNOSTICS @n = NUMBER diff --git a/parser/testdata/parser/mysql_unsupported_compound/output.sql b/parser/testdata/parser/mysql_unsupported_compound/output.sql deleted file mode 100644 index 140ba5b..0000000 --- a/parser/testdata/parser/mysql_unsupported_compound/output.sql +++ /dev/null @@ -1,5 +0,0 @@ --- error: line 1 column 6 near "SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'error'" --- case --- error: line 1 column 8 near "RESIGNAL" --- case --- error: line 1 column 3 near "GET DIAGNOSTICS @n = NUMBER" 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