From 8e073e6782bad84ecc9e2f8a737ee1a1b2647b67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 20 Aug 2026 23:58:00 +0000 Subject: [PATCH 01/13] Support BEGIN/COMMIT/ROLLBACK WORK, START TRANSACTION characteristic lists, and LOCK TABLES aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_txn coverage group (MySQL 26.7 §15.3.1, §15.3.6): its error goldens turn into Restore() goldens. - BEGIN WORK, COMMIT WORK, and ROLLBACK WORK accept the optional WORK keyword; Restore() drops it, matching the existing BEGIN -> START TRANSACTION canonicalization. - START TRANSACTION now parses a comma-separated transaction characteristic list (WITH CONSISTENT SNAPSHOT | READ WRITE | READ ONLY), keeping the TiDB READ ONLY AS OF and WITH CAUSAL CONSISTENCY ONLY extensions. - LOCK TABLES accepts [[AS] alias] per table (new TableLock.Alias field, restored with AS) and the deprecated LOW_PRIORITY WRITE lock type (new TableLockWriteLowPriority appended to the TableLockType enum). Keyword tables: WORK becomes an unreserved keyword; TestKeywordsLength count updated. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/ddl.go | 5 ++ ast/model.go | 5 ++ parser/keyword_classes.go | 1 + parser/keywords.go | 1 + parser/keywords_test.go | 4 +- parser/misc.go | 1 + parser/parse_misc.go | 15 +++- parser/parse_txn.go | 80 +++++++++++-------- .../parser/mysql_unsupported_txn/output.sql | 16 ++-- parser/token_kinds.go | 1 + 10 files changed, 86 insertions(+), 43 deletions(-) diff --git a/ast/ddl.go b/ast/ddl.go index 12f668b..2a91b8c 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -2301,6 +2301,7 @@ type LockTablesStmt struct { type TableLock struct { Table *TableName Type TableLockType + Alias CIStr // empty when absent; restored with AS } // Accept implements Node Accept interface. @@ -2330,6 +2331,10 @@ func (n *LockTablesStmt) Restore(ctx *format.RestoreCtx) error { if err := tl.Table.Restore(ctx); err != nil { return annotate(err, "An error occurred while add index") } + if tl.Alias.O != "" { + ctx.WriteKeyWord(" AS ") + ctx.WriteName(tl.Alias.O) + } ctx.WriteKeyWord(" " + tl.Type.String()) } return nil diff --git a/ast/model.go b/ast/model.go index f3ee139..6093cef 100644 --- a/ast/model.go +++ b/ast/model.go @@ -41,6 +41,9 @@ const ( TableLockWrite // TableLockWriteLocal means the session with this lock has write/read permission, and the other session still has read permission. TableLockWriteLocal + // TableLockWriteLowPriority is the deprecated LOW_PRIORITY WRITE lock. + // It affects only lock scheduling; the lock itself is a WRITE lock. + TableLockWriteLowPriority ) // String implements fmt.Stringer interface. @@ -58,6 +61,8 @@ func (t TableLockType) String() string { return "WRITE LOCAL" case TableLockWrite: return "WRITE" + case TableLockWriteLowPriority: + return "LOW_PRIORITY WRITE" } return "" } diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 1a993dc..59826ce 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -395,6 +395,7 @@ var unReservedKeywordNames = []string{ "HANDLER", "FOUND", "CALIBRATE", + "WORK", "WORKLOAD", "TPCC", "OLTP_READ_WRITE", diff --git a/parser/keywords.go b/parser/keywords.go index b0f38c5..ef79d14 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -729,6 +729,7 @@ var Keywords = []KeywordsType{ {"WEIGHT_STRING", false, "unreserved"}, {"WITHOUT", false, "unreserved"}, {"WITH_SYS_TABLE", false, "unreserved"}, + {"WORK", false, "unreserved"}, {"WORKLOAD", false, "unreserved"}, {"WRAPPER", false, "unreserved"}, {"X509", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 81fbaf6..dd4b4ea 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(759, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 759) + if !reflect.DeepEqual(760, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 760) } reservedNr := 0 diff --git a/parser/misc.go b/parser/misc.go index 646af7d..a1208f3 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -1025,6 +1025,7 @@ var tokenMap = map[string]int{ "WITHOUT": without, "WRAPPER": wrapper, "WRITE": write, + "WORK": work, "WORKLOAD": workload, "X509": x509, "XA": xa, diff --git a/parser/parse_misc.go b/parser/parse_misc.go index a57cf01..09d218a 100644 --- a/parser/parse_misc.go +++ b/parser/parse_misc.go @@ -348,9 +348,17 @@ func (r *rdParser) parseLockStmtFamily() ast.StmtNode { } } -// parseTableLock implements TableLock and LockType. +// parseTableLock implements TableLock and LockType: +// TableName [["AS"] Identifier] lock_type, with lock_type one of +// READ [LOCAL] | [LOW_PRIORITY] WRITE | WRITE LOCAL. func (r *rdParser) parseTableLock() ast.TableLock { tn := r.parseTableName() + var alias ast.CIStr + if r.accept(as) { + alias = ast.NewCIStr(r.parseIdentifier()) + } else if isIdentifierTok(r.tok()) { + alias = ast.NewCIStr(r.parseIdentifier()) + } var lockType ast.TableLockType switch r.tok() { case read: @@ -365,12 +373,17 @@ func (r *rdParser) parseTableLock() ast.TableLock { if r.accept(local) { lockType = ast.TableLockWriteLocal } + case lowPriority: + r.advance() + r.expect(write) + lockType = ast.TableLockWriteLowPriority default: r.syntaxError() } return ast.TableLock{ Table: tn, Type: lockType, + Alias: alias, } } diff --git a/parser/parse_txn.go b/parser/parse_txn.go index 355eb31..a14a9b0 100644 --- a/parser/parse_txn.go +++ b/parser/parse_txn.go @@ -50,48 +50,60 @@ func (r *rdParser) parseBeginTransactionStmt() ast.StmtNode { // "BEGIN" "OPTIMISTIC" r.advance() return &ast.BeginStmt{Mode: ast.Optimistic} + case work: + // "BEGIN" "WORK" + r.advance() + return &ast.BeginStmt{} } // "BEGIN" return &ast.BeginStmt{} } r.expect(start) r.expect(transaction) - switch r.tok() { - case read: - r.advance() - if r.accept(write) { - // "START" "TRANSACTION" "READ" "WRITE" - return &ast.BeginStmt{} - } - r.expect(only) - if r.tok() == asof { - // "START" "TRANSACTION" "READ" "ONLY" AsOfClause - return &ast.BeginStmt{ - ReadOnly: true, - AsOf: r.parseAsOfClause(), + stmt := &ast.BeginStmt{} + if r.tok() != read && r.tok() != with { + // "START" "TRANSACTION" + return stmt + } + // TransactionCharacteristicList: one or more comma-separated + // characteristics (WITH CONSISTENT SNAPSHOT | READ WRITE | READ ONLY), + // plus the TiDB extensions READ ONLY AsOfClause and WITH CAUSAL + // CONSISTENCY ONLY. + for { + switch r.tok() { + case read: + r.advance() + if r.accept(write) { + // "READ" "WRITE" + stmt.ReadOnly = false + } else { + r.expect(only) + stmt.ReadOnly = true + if r.tok() == asof { + // "READ" "ONLY" AsOfClause + stmt.AsOf = r.parseAsOfClause() + } } + case with: + r.advance() + if r.accept(consistent) { + // "WITH" "CONSISTENT" "SNAPSHOT" + r.expect(snapshot) + } else { + // "WITH" "CAUSAL" "CONSISTENCY" "ONLY" + r.expect(causal) + r.expect(consistency) + r.expect(only) + stmt.CausalConsistencyOnly = true + } + default: + r.syntaxError() } - // "START" "TRANSACTION" "READ" "ONLY" - return &ast.BeginStmt{ - ReadOnly: true, - } - case with: - r.advance() - if r.accept(consistent) { - // "START" "TRANSACTION" "WITH" "CONSISTENT" "SNAPSHOT" - r.expect(snapshot) - return &ast.BeginStmt{} - } - // "START" "TRANSACTION" "WITH" "CAUSAL" "CONSISTENCY" "ONLY" - r.expect(causal) - r.expect(consistency) - r.expect(only) - return &ast.BeginStmt{ - CausalConsistencyOnly: true, + if !r.accept(int(',')) { + break } } - // "START" "TRANSACTION" - return &ast.BeginStmt{} + return stmt } // parseCompletionType implements CompletionTypeWithinTransaction. @@ -136,6 +148,8 @@ func (r *rdParser) parseCompletionType() ast.CompletionType { // parseCommitStmt implements CommitStmt. func (r *rdParser) parseCommitStmt() ast.StmtNode { r.expect(commit) + // "COMMIT" "WORK" + r.accept(work) switch r.tok() { case and, release, no: // "COMMIT" CompletionTypeWithinTransaction @@ -147,6 +161,8 @@ func (r *rdParser) parseCommitStmt() ast.StmtNode { // parseRollbackStmt implements RollbackStmt. func (r *rdParser) parseRollbackStmt() ast.StmtNode { r.expect(rollback) + // "ROLLBACK" "WORK" + r.accept(work) switch r.tok() { case to: r.advance() diff --git a/parser/testdata/parser/mysql_unsupported_txn/output.sql b/parser/testdata/parser/mysql_unsupported_txn/output.sql index e00ab06..8e20b32 100644 --- a/parser/testdata/parser/mysql_unsupported_txn/output.sql +++ b/parser/testdata/parser/mysql_unsupported_txn/output.sql @@ -1,15 +1,15 @@ --- error: line 1 column 43 near ", READ ONLY" +START TRANSACTION READ ONLY -- case --- error: line 1 column 10 near "WORK" +START TRANSACTION -- case --- error: line 1 column 11 near "WORK" +COMMIT -- case --- error: line 1 column 13 near "WORK AND NO CHAIN RELEASE" +ROLLBACK RELEASE -- case --- error: line 1 column 13 near "WORK TO s1" +ROLLBACK TO s1 -- case --- error: line 1 column 17 near "AS a1 WRITE" +LOCK TABLES `t1` AS `a1` WRITE -- case --- error: line 1 column 17 near "a1 READ" +LOCK TABLES `t1` AS `a1` READ -- case --- error: line 1 column 27 near "LOW_PRIORITY WRITE" +LOCK TABLES `t2` LOW_PRIORITY WRITE diff --git a/parser/token_kinds.go b/parser/token_kinds.go index a6b7bfe..ab8f15f 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -1024,6 +1024,7 @@ const ( with = 57591 withSysTable = 57991 without = 57990 + work = 58328 workload = 57992 wrapper = 58319 write = 57592 From 2cc6f3775cc7cf70c80c7e354f196317369a8d29 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:00:07 +0000 Subject: [PATCH 02/13] Support SHOW STORAGE ENGINES, WARNINGS/ERRORS LIMIT, EXTENDED INDEX, and REPLICA STATUS FOR CHANNEL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_show coverage group (MySQL 26.7 §15.7.7): its error goldens turn into Restore() goldens. - SHOW STORAGE ENGINES parses; Restore() canonicalizes to SHOW ENGINES. - SHOW WARNINGS and SHOW ERRORS accept SelectStmtLimitOpt, stored in the existing ShowStmt.Limit field and restored after the keyword. - SHOW EXTENDED INDEX/INDEXES/KEYS FROM|IN parses, reusing ShowStmt.Extended; ShowIndex Restore() now writes the EXTENDED prefix. - SHOW REPLICA STATUS accepts FOR CHANNEL, stored in the existing ShowStmt.ChannelName field and restored after the keyword. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/dml.go | 19 ++++++++ parser/parse_show.go | 44 ++++++++++++++++--- .../parser/mysql_unsupported_show/output.sql | 14 +++--- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/ast/dml.go b/ast/dml.go index 1daf481..58ecc68 100644 --- a/ast/dml.go +++ b/ast/dml.go @@ -3599,6 +3599,9 @@ func (n *ShowStmt) Restore(ctx *format.RestoreCtx) error { case ShowIndex: // here can be INDEX INDEXES KEYS // FROM or IN + if n.Extended { + ctx.WriteKeyWord("EXTENDED ") + } ctx.WriteKeyWord("INDEX IN ") if err := n.Table.Restore(ctx); err != nil { return annotate(err, "An error occurred while restore ShowStmt.Table") @@ -3619,8 +3622,20 @@ func (n *ShowStmt) Restore(ctx *format.RestoreCtx) error { restoreShowDatabaseNameOpt() case ShowWarnings: ctx.WriteKeyWord("WARNINGS") + if n.Limit != nil { + ctx.WritePlain(" ") + if err := n.Limit.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ShowStmt.Limit") + } + } case ShowErrors: ctx.WriteKeyWord("ERRORS") + if n.Limit != nil { + ctx.WritePlain(" ") + if err := n.Limit.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ShowStmt.Limit") + } + } case ShowVariables: restoreGlobalScope() ctx.WriteKeyWord("VARIABLES") @@ -3699,6 +3714,10 @@ func (n *ShowStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("SESSION_STATES") case ShowReplicaStatus: ctx.WriteKeyWord("REPLICA STATUS") + if n.ChannelName != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteString(n.ChannelName) + } default: return errors.New("Unknown ShowStmt type") } diff --git a/parser/parse_show.go b/parser/parse_show.go index 5c91f94..d795808 100644 --- a/parser/parse_show.go +++ b/parser/parse_show.go @@ -236,11 +236,12 @@ func (r *rdParser) parseShowStmt() ast.StmtNode { } return stmt case replica, slave: - // "SHOW" Replica "STATUS" + // "SHOW" Replica "STATUS" ForChannelOpt r.advance() r.expect(status) return &ast.ShowStmt{ - Tp: ast.ShowReplicaStatus, + Tp: ast.ShowReplicaStatus, + ChannelName: r.parseForChannelOpt(), } case processlist: // "SHOW" OptFull "PROCESSLIST" (empty OptFull) @@ -703,6 +704,11 @@ func (r *rdParser) parseShowTargetFilterable() ast.StmtNode { case engines: r.advance() return r.applyShowLikeOrWhereOpt(&ast.ShowStmt{Tp: ast.ShowEngines}) + case storage: + // "STORAGE" "ENGINES"; Restore() canonicalizes to SHOW ENGINES. + r.advance() + r.expect(engines) + return r.applyShowLikeOrWhereOpt(&ast.ShowStmt{Tp: ast.ShowEngines}) case databases: r.advance() return r.applyShowLikeOrWhereOpt(&ast.ShowStmt{Tp: ast.ShowDatabases}) @@ -753,8 +759,26 @@ func (r *rdParser) parseShowTargetFilterable() ast.StmtNode { stmt.DBName = r.parseShowDatabaseNameOpt() return r.applyShowLikeOrWhereOpt(stmt) case extended: - // "EXTENDED" OptFull FieldsOrColumns ShowTableAliasOpt ShowDatabaseNameOpt + // "EXTENDED" ShowIndexKwd FromOrIn TableName + // | "EXTENDED" OptFull FieldsOrColumns ShowTableAliasOpt ShowDatabaseNameOpt r.advance() + if r.tok() == index || r.tok() == keys || r.tok() == indexes { + r.advance() + if r.tok() != from && r.tok() != in { + r.syntaxError() + } + r.advance() + tn := r.parseTableName() + if (r.tok() == from || r.tok() == in) && tn.Schema.O == "" { + r.advance() + tn = &ast.TableName{Name: tn.Name, Schema: ast.NewCIStr(r.parseIdentifier())} + } + return r.applyShowLikeOrWhereOpt(&ast.ShowStmt{ + Tp: ast.ShowIndex, + Table: tn, + Extended: true, + }) + } fullFlag := r.accept(full) if r.tok() != fields && r.tok() != columns { r.syntaxError() @@ -784,11 +808,21 @@ func (r *rdParser) parseShowTargetFilterable() ast.StmtNode { } r.syntaxError() case warnings: + // "WARNINGS" SelectStmtLimitOpt r.advance() - return r.applyShowLikeOrWhereOpt(&ast.ShowStmt{Tp: ast.ShowWarnings}) + stmt := &ast.ShowStmt{Tp: ast.ShowWarnings} + if r.tok() == limit || r.tok() == fetch { + stmt.Limit = r.parseSelectStmtLimit() + } + return r.applyShowLikeOrWhereOpt(stmt) case identSQLErrors: + // "ERRORS" SelectStmtLimitOpt r.advance() - return r.applyShowLikeOrWhereOpt(&ast.ShowStmt{Tp: ast.ShowErrors}) + stmt := &ast.ShowStmt{Tp: ast.ShowErrors} + if r.tok() == limit || r.tok() == fetch { + stmt.Limit = r.parseSelectStmtLimit() + } + return r.applyShowLikeOrWhereOpt(stmt) case global, session, variables, status, bindings: // GlobalScope ("VARIABLES" | "STATUS" | "BINDINGS") globalScope := false diff --git a/parser/testdata/parser/mysql_unsupported_show/output.sql b/parser/testdata/parser/mysql_unsupported_show/output.sql index 696fdac..73939dd 100644 --- a/parser/testdata/parser/mysql_unsupported_show/output.sql +++ b/parser/testdata/parser/mysql_unsupported_show/output.sql @@ -1,13 +1,13 @@ --- error: line 1 column 12 near "STORAGE ENGINES" +SHOW ENGINES -- case --- error: line 1 column 17 near "LIMIT 5" +SHOW ERRORS LIMIT 5 -- case --- error: line 1 column 17 near "LIMIT 5, 10" +SHOW ERRORS LIMIT 5,10 -- case --- error: line 1 column 19 near "LIMIT 1" +SHOW WARNINGS LIMIT 1 -- case --- error: line 1 column 19 near "LIMIT 5, 10" +SHOW WARNINGS LIMIT 5,10 -- case --- error: line 1 column 19 near "INDEX FROM t" +SHOW EXTENDED INDEX IN `t` -- case --- error: line 1 column 23 near "FOR CHANNEL 'ch'" +SHOW REPLICA STATUS FOR CHANNEL 'ch' From b4091f327e414a4fb1ecbfd2e602ca6cca3b1ede Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:03:47 +0000 Subject: [PATCH 03/13] Support CREATE TABLE AUTOEXTEND_SIZE/START TRANSACTION, index ENGINE_ATTRIBUTE, ALTER DATABASE READ ONLY, and bare WITH CHECK OPTION MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_ddl coverage group (MySQL 26.7 §15.1): its error goldens turn into Restore() goldens. - The AUTOEXTEND_SIZE table option now also accepts an integer literal value (previously only StringName forms like '4M' parsed). - CREATE TABLE ... START TRANSACTION parses as a new TableOptionStartTransaction table option (parsed and carried through Restore; like AUTOEXTEND_SIZE it has no storage-engine effect here). - ENGINE_ATTRIBUTE joins SECONDARY_ENGINE_ATTRIBUTE as an index option (new IndexOption.EngineAttr field). - ALTER DATABASE ... READ ONLY = {DEFAULT | 0 | 1} parses as a new DatabaseOptionReadOnly database option. - CREATE VIEW and ALTER VIEW accept WITH CHECK OPTION without a CASCADED/LOCAL qualifier, meaning CASCADED as in MySQL. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/ddl.go | 20 +++++++++++++++++++ parser/parse_alter.go | 3 +++ parser/parse_column.go | 9 ++++++++- parser/parse_create_misc.go | 15 ++++++++++++++ parser/parse_create_table.go | 19 +++++++++++++++++- parser/parse_create_view.go | 8 +++++--- parser/parse_mysql_ddl.go | 6 ++++-- .../parser/mysql_unsupported_ddl/output.sql | 14 ++++++------- 8 files changed, 80 insertions(+), 14 deletions(-) diff --git a/ast/ddl.go b/ast/ddl.go index 2a91b8c..128220d 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -82,6 +82,7 @@ const ( DatabaseOptionCollate DatabaseOptionEncryption DatabaseSetTiFlashReplica + DatabaseOptionReadOnly DatabaseOptionPlacementPolicy = DatabaseOptionType(PlacementOptionPolicy) ) @@ -108,6 +109,10 @@ func (n *DatabaseOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("ENCRYPTION") ctx.WritePlain(" = ") ctx.WriteString(n.Value) + case DatabaseOptionReadOnly: + ctx.WriteKeyWord("READ ONLY") + ctx.WritePlain(" = ") + ctx.WriteKeyWord(n.Value) case DatabaseOptionPlacementPolicy: placementOpt := PlacementOption{ Tp: PlacementOptionPolicy, @@ -754,6 +759,7 @@ type IndexOption struct { PrimaryKeyTp PrimaryKeyType Global bool SplitOpt *SplitOption `json:"-"` // SplitOption contains expr nodes, which cannot marshal for DDL job arguments. + EngineAttr string SecondaryEngineAttr string AddColumnarReplicaOnDemand int Condition ExprNode `json:"-"` // Condition contains expr nodes, which cannot marshal for DDL job arguments. It's used for partial index. @@ -770,6 +776,7 @@ func (n *IndexOption) IsEmpty() bool { n.Global || n.Visibility != IndexVisibilityDefault || n.SplitOpt != nil || + len(n.EngineAttr) > 0 || len(n.SecondaryEngineAttr) > 0 || n.Condition != nil { return false @@ -877,6 +884,16 @@ func (n *IndexOption) Restore(ctx *format.RestoreCtx) error { hasPrevOption = true } + if n.EngineAttr != "" { + if hasPrevOption { + ctx.WritePlain(" ") + } + ctx.WriteKeyWord("ENGINE_ATTRIBUTE") + ctx.WritePlain(" = ") + ctx.WriteString(n.EngineAttr) + hasPrevOption = true + } + if n.SecondaryEngineAttr != "" { if hasPrevOption { ctx.WritePlain(" ") @@ -2888,6 +2905,7 @@ const ( TableOptionIetfQuotes TableOptionSequence TableOptionAffinity + TableOptionStartTransaction TableOptionPlacementPolicy = TableOptionType(PlacementOptionPolicy) TableOptionStatsBuckets = TableOptionType(StatsOptionBuckets) TableOptionStatsTopN = TableOptionType(StatsOptionTopN) @@ -3275,6 +3293,8 @@ func (n *TableOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("AUTOEXTEND_SIZE ") ctx.WritePlain("= ") ctx.WritePlain(n.StrValue) // e.g. '4M' + case TableOptionStartTransaction: + ctx.WriteKeyWord("START TRANSACTION") // MariaDB specific options case TableOptionPageChecksum: diff --git a/parser/parse_alter.go b/parser/parse_alter.go index 256d63d..ef9cace 100644 --- a/parser/parse_alter.go +++ b/parser/parse_alter.go @@ -1462,6 +1462,9 @@ func (r *rdParser) isAlterDatabaseOptionListStart() bool { switch r.tok() { case defaultKwd, collate, set, placement, character, charType: return true + case read: + // "READ" "ONLY" + return r.la(1) == only } return false } diff --git a/parser/parse_column.go b/parser/parse_column.go index 9565502..5b2cbb9 100644 --- a/parser/parse_column.go +++ b/parser/parse_column.go @@ -1019,7 +1019,7 @@ func (r *rdParser) isIndexOptionStart() bool { switch r.tok() { case keyBlockSize, addColumnarReplicaOnDemand, using, tp, with, comment, visible, invisible, clustered, nonclustered, global, local, - preSplitRegions, secondaryEngineAttribute, where: + preSplitRegions, engine_attribute, secondaryEngineAttribute, where: return true } return false @@ -1054,6 +1054,8 @@ func (r *rdParser) parseIndexOptionList() *ast.IndexOption { opt1.Global = true } else if opt2.SplitOpt != nil { opt1.SplitOpt = opt2.SplitOpt + } else if len(opt2.EngineAttr) > 0 { + opt1.EngineAttr = opt2.EngineAttr } else if len(opt2.SecondaryEngineAttr) > 0 { opt1.SecondaryEngineAttr = opt2.SecondaryEngineAttr } else if opt2.Condition != nil { @@ -1145,6 +1147,11 @@ func (r *rdParser) parseIndexOption() *ast.IndexOption { Num: r.parseInt64Num(), }, } + case engine_attribute: + // "ENGINE_ATTRIBUTE" EqOpt stringLit + r.advance() + r.parseEqOpt() + return &ast.IndexOption{EngineAttr: r.expect(stringLit).lit} case secondaryEngineAttribute: // "SECONDARY_ENGINE_ATTRIBUTE" EqOpt stringLit r.advance() diff --git a/parser/parse_create_misc.go b/parser/parse_create_misc.go index a9b5bda..6385019 100644 --- a/parser/parse_create_misc.go +++ b/parser/parse_create_misc.go @@ -66,6 +66,9 @@ func (r *rdParser) isDatabaseOptionStart() bool { case set: // "SET" "TIFLASH" "REPLICA" ... return r.la(1) == tiFlash + case read: + // "READ" "ONLY" (ALTER DATABASE) + return r.la(1) == only } return false } @@ -96,6 +99,18 @@ func (r *rdParser) parseDatabaseOption() *ast.DatabaseOption { Value: placementOptions.StrValue, UintValue: placementOptions.UintValue, } + case read: + // "READ" "ONLY" EqOpt ("DEFAULT" | Int64Num) + r.advance() + r.expect(only) + r.parseEqOpt() + if r.accept(defaultKwd) { + return &ast.DatabaseOption{Tp: ast.DatabaseOptionReadOnly, Value: "DEFAULT"} + } + return &ast.DatabaseOption{ + Tp: ast.DatabaseOptionReadOnly, + Value: strconv.FormatInt(r.parseInt64Num(), 10), + } } // DefaultKwdOpt r.accept(defaultKwd) diff --git a/parser/parse_create_table.go b/parser/parse_create_table.go index 017a861..21a65a2 100644 --- a/parser/parse_create_table.go +++ b/parser/parse_create_table.go @@ -20,6 +20,7 @@ package parser import ( "fmt" + "strconv" "strings" "github.com/sqlc-dev/marino/ast" @@ -364,6 +365,9 @@ func (r *rdParser) isTableOptionStart() bool { case character, charType: // CharsetKw: "CHARACTER" "SET" | "CHAR" "SET" return r.la(1) == set + case start: + // "START" "TRANSACTION" + return r.la(1) == transaction } return false } @@ -648,9 +652,22 @@ func (r *rdParser) parseTableOption() *ast.TableOption { // Parse it but will ignore it. r.advance() r.parseEqOpt() - opt := &ast.TableOption{Tp: ast.TableOptionAutoextendSize, StrValue: r.parseStringName()} + var v string + if r.tok() == intLit { + v = strconv.FormatUint(getUint64FromNUM(r.cur().item), 10) + r.advance() + } else { + v = r.parseStringName() + } + opt := &ast.TableOption{Tp: ast.TableOptionAutoextendSize, StrValue: v} r.appendWarnf("The AUTOEXTEND_SIZE option is parsed but ignored by all storage engines.") return opt + case start: + // "START" "TRANSACTION" (CREATE TABLE only; parsed and ignored, + // like AUTOEXTEND_SIZE). + r.advance() + r.expect(transaction) + return &ast.TableOption{Tp: ast.TableOptionStartTransaction} case affinity: r.advance() r.parseEqOpt() diff --git a/parser/parse_create_view.go b/parser/parse_create_view.go index f84f6a6..29bd8c1 100644 --- a/parser/parse_create_view.go +++ b/parser/parse_create_view.go @@ -111,18 +111,20 @@ func (r *rdParser) parseCreateViewStmt() ast.StmtNode { if cols != nil { x.Cols = cols } - // ViewCheckOption: empty | "WITH" ("CASCADED"|"LOCAL") "CHECK" "OPTION" + // ViewCheckOption: empty | "WITH" [("CASCADED"|"LOCAL")] "CHECK" "OPTION"; + // a bare WITH CHECK OPTION means CASCADED, as in MySQL. if r.tok() == with { r.advance() switch r.tok() { case cascaded: x.CheckOption = ast.CheckOptionCascaded + r.advance() case local: x.CheckOption = ast.CheckOptionLocal + r.advance() default: - r.syntaxError() + x.CheckOption = ast.CheckOptionCascaded } - r.advance() r.expect(check) r.expect(option) } else { diff --git a/parser/parse_mysql_ddl.go b/parser/parse_mysql_ddl.go index e13c9dc..3ba5d77 100644 --- a/parser/parse_mysql_ddl.go +++ b/parser/parse_mysql_ddl.go @@ -479,17 +479,19 @@ func (r *rdParser) parseAlterViewStmt() ast.StmtNode { if cols != nil { x.Cols = cols } + // A bare WITH CHECK OPTION means CASCADED, as in MySQL. if r.tok() == with { r.advance() switch r.tok() { case cascaded: x.CheckOption = ast.CheckOptionCascaded + r.advance() case local: x.CheckOption = ast.CheckOptionLocal + r.advance() default: - r.syntaxError() + x.CheckOption = ast.CheckOptionCascaded } - r.advance() r.expect(check) r.expect(option) } else { diff --git a/parser/testdata/parser/mysql_unsupported_ddl/output.sql b/parser/testdata/parser/mysql_unsupported_ddl/output.sql index 87115a1..66927a9 100644 --- a/parser/testdata/parser/mysql_unsupported_ddl/output.sql +++ b/parser/testdata/parser/mysql_unsupported_ddl/output.sql @@ -1,13 +1,13 @@ --- error: line 1 column 48 near "4194304" +CREATE TABLE `t` (`a` INT) AUTOEXTEND_SIZE = 4194304 -- case --- error: line 1 column 28 near "START TRANSACTION" +CREATE TABLE `t` (`a` INT) START TRANSACTION -- case --- error: line 1 column 41 near "ENGINE_ATTRIBUTE = '{}'" +CREATE INDEX `i1` ON `t` (`a`) ENGINE_ATTRIBUTE = '{}' -- case --- error: line 1 column 21 near "READ ONLY = 1" +ALTER DATABASE `d` READ ONLY = 1 -- case --- error: line 1 column 21 near "READ ONLY = DEFAULT" +ALTER DATABASE `d` READ ONLY = DEFAULT -- case --- error: line 1 column 36 near "CHECK OPTION" +CREATE ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 -- case --- error: line 1 column 35 near "CHECK OPTION" +ALTER ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 From 80b40c975133462d97e4f97ed15891a352e58bf5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:06:13 +0000 Subject: [PATCH 04/13] Support non-literal CHANGE REPLICATION SOURCE option values and RESET BINARY LOGS AND GTIDS TO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_replication coverage group (MySQL 26.7 §15.4.2.1, §15.4.1.2): its error goldens turn into Restore() goldens. ReplicationSourceOption gains three value forms beyond literals, each a new backwards-compatible field selected by the option name: - IGNORE_SERVER_IDS = (n, ...) parses into ServerIDs (non-nil but empty for an empty list, so () round-trips). - PRIVILEGE_CHECKS_USER takes an account name (User) or NULL. - REQUIRE_TABLE_PRIMARY_KEY_CHECK and ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS take bare keyword values (STREAM, GENERATE, ON, OFF, LOCAL), stored uppercase in KeywordValue; the ASSIGN_GTIDS uuid string form stays a literal Value. ChangeReplicationSourceStmt.Accept now skips options without a literal Value instead of dereferencing nil. RESET BINARY LOGS AND GTIDS accepts the TO binary_log_file_index_number clause (new ResetBinaryLogsAndGtidsStmt.To field, zero when absent). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/misc.go | 42 ++++++++++++++-- ast/replication.go | 9 +++- parser/parse_replication.go | 48 +++++++++++++++++-- .../mysql_unsupported_replication/output.sql | 22 ++++----- 4 files changed, 102 insertions(+), 19 deletions(-) diff --git a/ast/misc.go b/ast/misc.go index 8514af0..9021105 100644 --- a/ast/misc.go +++ b/ast/misc.go @@ -859,15 +859,48 @@ func (n *BinlogStmt) Accept(v Visitor) (Node, bool) { // ReplicationSourceOption is a single name = value option of // ChangeReplicationSourceStmt. Names are stored uppercase; the parser // does not validate them against the server's option list. Values are -// literals: a string, integer, or decimal. +// usually literals (a string, integer, or decimal, in Value); the +// non-literal forms are a bare keyword (KeywordValue, e.g. +// REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM or PRIVILEGE_CHECKS_USER = +// NULL), an account name (User, PRIVILEGE_CHECKS_USER = 'u'@'h'), and a +// parenthesized server-id list (ServerIDs, IGNORE_SERVER_IDS = (1, 2); +// non-nil but empty for an empty list). type ReplicationSourceOption struct { - Name string - Value ValueExpr + Name string + Value ValueExpr + KeywordValue string + User *auth.UserIdentity + ServerIDs []ValueExpr } // Restore implements Node interface. func (n *ReplicationSourceOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord(n.Name) + if n.ServerIDs != nil { + ctx.WritePlain(" = (") + for i, id := range n.ServerIDs { + if i != 0 { + ctx.WritePlain(", ") + } + if err := id.Restore(ctx); err != nil { + return fmt.Errorf("an error occurred while restore ReplicationSourceOption.ServerIDs[%d]: %w", i, err) + } + } + ctx.WritePlain(")") + return nil + } + if n.User != nil { + ctx.WritePlain(" = ") + if err := n.User.Restore(ctx); err != nil { + return fmt.Errorf("an error occurred while restore ReplicationSourceOption.User: %w", err) + } + return nil + } + if n.KeywordValue != "" { + ctx.WritePlain(" = ") + ctx.WriteKeyWord(n.KeywordValue) + return nil + } if n.Value == nil { // A bare option name (START REPLICA UNTIL SQL_AFTER_MTS_GAPS). return nil @@ -934,6 +967,9 @@ func (n *ChangeReplicationSourceStmt) Accept(v Visitor) (Node, bool) { } n = newNode.(*ChangeReplicationSourceStmt) for _, opt := range n.Options { + if opt.Value == nil { + continue + } node, ok := opt.Value.Accept(v) if !ok { return n, false diff --git a/ast/replication.go b/ast/replication.go index 8bfd5bc..6239183 100644 --- a/ast/replication.go +++ b/ast/replication.go @@ -83,14 +83,21 @@ func (n *PurgeBinaryLogsStmt) Accept(v Visitor) (Node, bool) { // ResetBinaryLogsAndGtidsStmt is a RESET BINARY LOGS AND GTIDS // statement (which replaced RESET MASTER; the removed spelling is not -// parsed). +// parsed). To is the TO binary_log_file_index_number clause; zero when +// absent (MySQL requires the index to be positive). type ResetBinaryLogsAndGtidsStmt struct { stmtNode + + To int64 } // Restore implements Node interface. func (n *ResetBinaryLogsAndGtidsStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("RESET BINARY LOGS AND GTIDS") + if n.To != 0 { + ctx.WriteKeyWord(" TO ") + ctx.WritePlainf("%d", n.To) + } return nil } diff --git a/parser/parse_replication.go b/parser/parse_replication.go index fcbb465..7730a3d 100644 --- a/parser/parse_replication.go +++ b/parser/parse_replication.go @@ -114,11 +114,46 @@ func (r *rdParser) parseChangeReplicationSourceStmt() ast.StmtNode { } // parseReplicationSourceOption implements ReplicationSourceOption: -// Identifier eq (stringLit | intLit | decLit | floatLit). +// Identifier eq (stringLit | intLit | decLit | floatLit), plus the +// non-literal value forms selected by the option name: a parenthesized +// server-id list (IGNORE_SERVER_IDS), an account name or NULL +// (PRIVILEGE_CHECKS_USER), and bare keyword values +// (REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM|GENERATE|ON|OFF, +// ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = OFF|LOCAL|uuid). func (r *rdParser) parseReplicationSourceOption() *ast.ReplicationSourceOption { name := strings.ToUpper(r.parseIdentifier()) r.expect(eq) - return &ast.ReplicationSourceOption{Name: name, Value: r.parseReplicationOptionValue()} + opt := &ast.ReplicationSourceOption{Name: name} + switch name { + case "IGNORE_SERVER_IDS": + r.expect(int('(')) + opt.ServerIDs = []ast.ValueExpr{} + if r.tok() != int(')') { + opt.ServerIDs = append(opt.ServerIDs, r.parseReplicationOptionValue()) + for r.accept(int(',')) { + opt.ServerIDs = append(opt.ServerIDs, r.parseReplicationOptionValue()) + } + } + r.expect(int(')')) + case "PRIVILEGE_CHECKS_USER": + if r.accept(null) { + opt.KeywordValue = "NULL" + } else { + opt.User = r.parseGrantUsername() + } + case "REQUIRE_TABLE_PRIMARY_KEY_CHECK", "ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS": + if r.accept(on) { + opt.KeywordValue = "ON" + } else if r.tok() == stringLit { + // ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = 'uuid' + opt.Value = r.parseReplicationOptionValue() + } else { + opt.KeywordValue = strings.ToUpper(r.parseIdentifier()) + } + default: + opt.Value = r.parseReplicationOptionValue() + } + return opt } // parseReplicationOptionValue implements the literal value of a @@ -275,14 +310,19 @@ func (r *rdParser) parseResetReplicaStmt() ast.StmtNode { } // parseResetBinaryLogsAndGtidsStmt implements -// ResetBinaryLogsAndGtidsStmt: "RESET" "BINARY" "LOGS" "AND" "GTIDS". +// ResetBinaryLogsAndGtidsStmt: +// "RESET" "BINARY" "LOGS" "AND" "GTIDS" ["TO" Int64Num]. func (r *rdParser) parseResetBinaryLogsAndGtidsStmt() ast.StmtNode { r.expect(reset) r.expect(binaryType) r.expect(logs) r.expect(and) r.expect(gtids) - return &ast.ResetBinaryLogsAndGtidsStmt{} + stmt := &ast.ResetBinaryLogsAndGtidsStmt{} + if r.accept(to) { + stmt.To = r.parseInt64Num() + } + return stmt } // parseReplicaThreadTypes implements the thread_types list of diff --git a/parser/testdata/parser/mysql_unsupported_replication/output.sql b/parser/testdata/parser/mysql_unsupported_replication/output.sql index 72147c8..7db9134 100644 --- a/parser/testdata/parser/mysql_unsupported_replication/output.sql +++ b/parser/testdata/parser/mysql_unsupported_replication/output.sql @@ -1,21 +1,21 @@ --- error: line 1 column 50 near "(1, 2)" +CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = (1, 2) -- case --- error: line 1 column 50 near "()" +CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = () -- case --- error: line 1 column 60 near "@'h'" +CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = `u`@`h` -- case --- error: line 1 column 57 near "NULL" +CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = NULL -- case --- error: line 1 column 65 near "ON" +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = ON -- case --- error: line 1 column 66 near "OFF" +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = OFF -- case --- error: line 1 column 69 near "STREAM" +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM -- case --- error: line 1 column 71 near "GENERATE" +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE -- case --- error: line 1 column 73 near "OFF" +CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = OFF -- case --- error: line 1 column 75 near "LOCAL" +CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = LOCAL -- case --- error: line 1 column 30 near "TO 100" +RESET BINARY LOGS AND GTIDS TO 100 From 25c6685eddba5024ad0a6adf5997eb3f83939bf5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:09:10 +0000 Subject: [PATCH 05/13] Support EXPLAIN FORMAT = TREE, EXPLAIN ... INTO @var, and DESCRIBE table wildcards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_utility coverage group (MySQL 26.7 §15.8.2): its error goldens turn into Restore() goldens. - The FORMAT = value of EXPLAIN [ANALYZE] now also accepts a bare identifier (e.g. TREE), yielding its spelling like the keyword alternatives; Restore() writes formats as quoted strings, unchanged. - EXPLAIN FORMAT = ... INTO @var parses into the new ExplainStmt.IntoVar field, restored between the format and the explained statement. - DESCRIBE tbl 'wildcard' parses the column wildcard into the inner ShowStmt's existing Pattern field (a LIKE pattern, as SHOW COLUMNS does); the DESC form of ExplainStmt Restore() writes it as a plain string since DESC does not accept a charset-prefixed literal. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/misc.go | 18 ++++++++++++ parser/parse_misc.go | 28 ++++++++++++++++--- .../mysql_unsupported_utility/output.sql | 10 +++---- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/ast/misc.go b/ast/misc.go index 9021105..cfcdf39 100644 --- a/ast/misc.go +++ b/ast/misc.go @@ -219,6 +219,9 @@ type ExplainStmt struct { SQLDigest string // PlanDigest to explain, used in `EXPLAIN [ANALYZE] `. PlanDigest string + // IntoVar is the user variable of `EXPLAIN FORMAT = ... INTO @var`, + // without the leading @; empty when absent. + IntoVar string } // Restore implements Node interface. @@ -233,6 +236,15 @@ func (n *ExplainStmt) Restore(ctx *format.RestoreCtx) error { if err := showStmt.Column.Restore(ctx); err != nil { return annotate(err, "An error occurred while restore ExplainStmt.ShowStmt.Column") } + } else if showStmt.Pattern != nil && showStmt.Pattern.Pattern != nil { + ctx.WritePlain(" ") + if pat, ok := showStmt.Pattern.Pattern.(ValueExpr); ok { + // Write the wildcard as a plain string: DESC does not + // accept a charset-prefixed literal. + ctx.WriteString(pat.GetString()) + } else if err := showStmt.Pattern.Pattern.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore ExplainStmt.ShowStmt.Pattern") + } } return nil } @@ -251,6 +263,12 @@ func (n *ExplainStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteString(n.Format) ctx.WritePlain(" ") } + if n.IntoVar != "" { + ctx.WriteKeyWord("INTO ") + ctx.WritePlain("@") + ctx.WriteName(n.IntoVar) + ctx.WritePlain(" ") + } if n.PlanDigest != "" { ctx.WriteString(n.PlanDigest) } diff --git a/parser/parse_misc.go b/parser/parse_misc.go index 09d218a..dedc0d2 100644 --- a/parser/parse_misc.go +++ b/parser/parse_misc.go @@ -574,10 +574,17 @@ func (r *rdParser) parseExplainStmt() ast.StmtNode { Format: formatStr, } } - // ... ExplainableStmt + // ... ["INTO" UserVariable] ExplainableStmt + var intoVar string + if r.tok() == into && r.la(1) == singleAtIdentifier { + r.advance() + intoVar = strings.TrimPrefix(r.cur().lit, "@") + r.advance() + } return &ast.ExplainStmt{ - Stmt: r.parseExplainableStmt(), - Format: formatStr, + Stmt: r.parseExplainableStmt(), + Format: formatStr, + IntoVar: intoVar, } } if r.tok() == stringLit { @@ -597,13 +604,21 @@ func (r *rdParser) parseExplainStmt() ast.StmtNode { Format: "row", } } - // ExplainSym TableName [ColumnName] + // ExplainSym TableName [ColumnName | stringLit] showStmt := &ast.ShowStmt{ Tp: ast.ShowColumns, Table: r.parseTableName(), } if isIdentifierTok(r.tok()) { showStmt.Column = r.parseColumnName() + } else if r.tok() == stringLit { + // A column-name wildcard pattern, as in SHOW COLUMNS ... LIKE. + showStmt.Pattern = &ast.PatternLikeOrIlikeExpr{ + Pattern: r.parseSimpleExpr(), + Escape: '\\', + EscapeExplicit: false, + IsLike: true, + } } return &ast.ExplainStmt{ Stmt: showStmt, @@ -612,6 +627,8 @@ func (r *rdParser) parseExplainStmt() ast.StmtNode { // parseExplainFormat parses the symbol after "FORMAT" "=": either a // stringLit or an ExplainFormatType keyword; both yield their spelling. +// A bare identifier (e.g. the MySQL TREE format) also parses, yielding +// its spelling. func (r *rdParser) parseExplainFormat() string { switch r.tok() { case stringLit, traditional, jsonType, row, dotType, briefType, verboseType, trueCardCost, tidbJson: @@ -619,6 +636,9 @@ func (r *rdParser) parseExplainFormat() string { r.advance() return lit } + if isIdentifierTok(r.tok()) { + return r.parseIdentifier() + } r.syntaxError() return "" } diff --git a/parser/testdata/parser/mysql_unsupported_utility/output.sql b/parser/testdata/parser/mysql_unsupported_utility/output.sql index 190a8d5..5167c54 100644 --- a/parser/testdata/parser/mysql_unsupported_utility/output.sql +++ b/parser/testdata/parser/mysql_unsupported_utility/output.sql @@ -1,9 +1,9 @@ --- error: line 1 column 21 near "TREE SELECT 1" +EXPLAIN FORMAT = 'TREE' SELECT 1 -- case --- error: line 1 column 29 near "TREE SELECT 1" +EXPLAIN ANALYZE FORMAT = 'TREE' SELECT 1 -- case --- error: line 1 column 26 near "INTO @plan SELECT 1" +EXPLAIN FORMAT = 'JSON' INTO @`plan` SELECT 1 -- case --- error: line 1 column 26 near "INTO @plan SELECT t.id, COUNT(*) FROM information_schema.tables t GROUP BY t.id" +EXPLAIN FORMAT = 'JSON' INTO @`plan` SELECT `t`.`id`,COUNT(1) FROM `information_schema`.`tables` AS `t` GROUP BY `t`.`id` -- case --- error: line 1 column 14 near "'a%'" +DESC `t` 'a%' From 062d7759a70e62d6bafd4ba9e2be465776999fa6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:15:07 +0000 Subject: [PATCH 06/13] Support histogram options, INSTALL COMPONENT SET, resource-group extensions, FLUSH variants, and SET PERSIST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_admin coverage group (MySQL 26.7 §15.7): its error goldens turn into Restore() goldens. - ANALYZE TABLE ... UPDATE HISTOGRAM gains USING DATA 'json' (new AnalyzeTableStmt.HistogramData field) and the AUTO/MANUAL UPDATE clause (new HistogramUpdate field with HistogramUpdateType), composing with WITH n BUCKETS. - INSTALL COMPONENT ... SET now works with PERSIST-scoped assignments (see below). - DROP RESOURCE GROUP accepts FORCE (new field); SET RESOURCE GROUP accepts FOR thread_id, ... (new ThreadIDs field). - FLUSH gains OPTIMIZER_COSTS and USER_RESOURCES option types, RELAY LOGS [FOR CHANNEL ch] (new LogTypeRelay and Channel field), TABLES ... FOR EXPORT (new ForExport field), and comma-separated option lists (new MoreOptions field; the option rendering split into restoreOption). - SET PERSIST / PERSIST_ONLY parse as new VariableAssignment IsPersist/IsPersistOnly flags (also implying IsGlobal), restored as @@PERSIST./@@PERSIST_ONLY. system variables; the lexer's @@ prefix list learns those spellings so the restored form round-trips. Scopes mix freely in one SET list. Keyword tables: AUTO, EXPORT, OPTIMIZER_COSTS, PERSIST_ONLY, RELAY, and USER_RESOURCES become unreserved keywords; TestKeywordsLength counts updated. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/ddl.go | 4 ++ ast/misc.go | 70 ++++++++++++++++--- ast/stats.go | 37 ++++++++++ parser/keyword_classes.go | 6 ++ parser/keywords.go | 6 ++ parser/keywords_test.go | 4 +- parser/lexer.go | 2 +- parser/misc.go | 6 ++ parser/parse_analyze.go | 25 ++++++- parser/parse_drop.go | 3 +- parser/parse_misc.go | 30 ++++++-- parser/parse_set.go | 39 ++++++++++- .../parser/mysql_unsupported_admin/output.sql | 36 +++++----- parser/token_kinds.go | 6 ++ 14 files changed, 234 insertions(+), 40 deletions(-) diff --git a/ast/ddl.go b/ast/ddl.go index 128220d..7a93caa 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -1482,6 +1482,7 @@ type DropResourceGroupStmt struct { IfExists bool ResourceGroupName CIStr + Force bool } // Restore implements Restore interface. @@ -1495,6 +1496,9 @@ func (n *DropResourceGroupStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("IF EXISTS ") } ctx.WriteName(n.ResourceGroupName.O) + if n.Force { + ctx.WriteKeyWord(" FORCE") + } return nil } diff --git a/ast/misc.go b/ast/misc.go index cfcdf39..37e33fd 100644 --- a/ast/misc.go +++ b/ast/misc.go @@ -1119,11 +1119,13 @@ const ( // VariableAssignment is a variable assignment struct. type VariableAssignment struct { node - Name string - Value ExprNode - IsInstance bool - IsGlobal bool - IsSystem bool + Name string + Value ExprNode + IsInstance bool + IsGlobal bool + IsSystem bool + IsPersist bool // SET PERSIST var: persist to mysqld-auto.cnf and set globally + IsPersistOnly bool // SET PERSIST_ONLY var: persist without setting the running value // ExtendValue is a way to store extended info. // VariableAssignment should be able to store information for SetCharset/SetPWD Stmt. @@ -1136,7 +1138,11 @@ type VariableAssignment struct { func (n *VariableAssignment) Restore(ctx *format.RestoreCtx) error { if n.IsSystem { ctx.WritePlain("@@") - if n.IsGlobal { + if n.IsPersist { + ctx.WriteKeyWord("PERSIST") + } else if n.IsPersistOnly { + ctx.WriteKeyWord("PERSIST_ONLY") + } else if n.IsGlobal { ctx.WriteKeyWord("GLOBAL") } else if n.IsInstance { ctx.WriteKeyWord("INSTANCE") @@ -1199,6 +1205,8 @@ const ( FlushLogs FlushClientErrorsSummary FlushStatsDelta + FlushOptimizerCosts + FlushUserResources ) // LogType is the log type used in FLUSH statement. @@ -1211,6 +1219,7 @@ const ( LogTypeError LogTypeGeneral LogTypeSlow + LogTypeRelay ) // FlushStmt is a statement to flush tables/privileges/optimizer costs and so on. @@ -1225,6 +1234,12 @@ type FlushStmt struct { Plugins []string IsCluster bool // For FlushStatsDelta, whether to flush cluster-wide stats delta FlushObjects []*StatsObject // For FlushStatsDelta, scoped objects (db.tbl, db.*, *.*). Always non-empty. + Channel string // For FLUSH RELAY LOGS FOR CHANNEL; empty when absent. + ForExport bool // For FLUSH TABLES ... FOR EXPORT. + // MoreOptions are the second and later options of a comma-separated + // FLUSH option list (FLUSH BINARY LOGS, STATUS, ...); each carries + // only its option fields, not NoWriteToBinLog. + MoreOptions []*FlushStmt } // Restore implements Node interface. @@ -1233,6 +1248,21 @@ func (n *FlushStmt) Restore(ctx *format.RestoreCtx) error { if n.NoWriteToBinLog { ctx.WriteKeyWord("NO_WRITE_TO_BINLOG ") } + if err := n.restoreOption(ctx); err != nil { + return err + } + for i, opt := range n.MoreOptions { + ctx.WritePlain(", ") + if err := opt.restoreOption(ctx); err != nil { + return annotatef(err, "An error occurred while restore FlushStmt.MoreOptions[%d]", i) + } + } + return nil +} + +// restoreOption writes one FLUSH option (everything after FLUSH +// [NO_WRITE_TO_BINLOG] for this statement's own option). +func (n *FlushStmt) restoreOption(ctx *format.RestoreCtx) error { switch n.Tp { case FlushTables: ctx.WriteKeyWord("TABLES") @@ -1249,6 +1279,9 @@ func (n *FlushStmt) Restore(ctx *format.RestoreCtx) error { if n.ReadLock { ctx.WriteKeyWord(" WITH READ LOCK") } + if n.ForExport { + ctx.WriteKeyWord(" FOR EXPORT") + } case FlushPrivileges: ctx.WriteKeyWord("PRIVILEGES") case FlushStatus: @@ -1280,8 +1313,18 @@ func (n *FlushStmt) Restore(ctx *format.RestoreCtx) error { logType = "GENERAL LOGS" case LogTypeSlow: logType = "SLOW LOGS" + case LogTypeRelay: + logType = "RELAY LOGS" } ctx.WriteKeyWord(logType) + if n.Channel != "" { + ctx.WriteKeyWord(" FOR CHANNEL ") + ctx.WriteString(n.Channel) + } + case FlushOptimizerCosts: + ctx.WriteKeyWord("OPTIMIZER_COSTS") + case FlushUserResources: + ctx.WriteKeyWord("USER_RESOURCES") case FlushClientErrorsSummary: ctx.WriteKeyWord("CLIENT_ERRORS_SUMMARY") case FlushStatsDelta: @@ -4410,15 +4453,26 @@ type BinaryLiteral interface { ToString() string } -// SetResourceGroupStmt is a statement to set the resource group name for current session. +// SetResourceGroupStmt is a statement to set the resource group name for +// the current session, or for the given threads (FOR thread_id, ...). type SetResourceGroupStmt struct { stmtNode - Name CIStr + Name CIStr + ThreadIDs []int64 // FOR thread_id list; nil when absent } func (n *SetResourceGroupStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("SET RESOURCE GROUP ") ctx.WriteName(n.Name.O) + if len(n.ThreadIDs) > 0 { + ctx.WriteKeyWord(" FOR ") + for i, id := range n.ThreadIDs { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WritePlainf("%d", id) + } + } return nil } diff --git a/ast/stats.go b/ast/stats.go index 6756ae2..edbe701 100644 --- a/ast/stats.go +++ b/ast/stats.go @@ -46,6 +46,12 @@ type AnalyzeTableStmt struct { // ColumnNames indicate the columns whose statistics need to be collected. ColumnNames []CIStr ColumnChoice ColumnChoice + // HistogramData is the USING DATA 'json' value of UPDATE HISTOGRAM; + // empty when absent. + HistogramData string + // HistogramUpdate is the AUTO UPDATE / MANUAL UPDATE clause of + // UPDATE HISTOGRAM. + HistogramUpdate HistogramUpdateType } // AnalyzeOptType is the type for analyze options. @@ -95,6 +101,29 @@ func (hot HistogramOperationType) String() string { return "" } +// HistogramUpdateType is the AUTO UPDATE / MANUAL UPDATE clause of +// ANALYZE TABLE ... UPDATE HISTOGRAM. +type HistogramUpdateType int + +// Histogram update types. +const ( + // HistogramUpdateNop means the clause is absent. Default value. + HistogramUpdateNop HistogramUpdateType = iota + HistogramUpdateAuto + HistogramUpdateManual +) + +// String implements fmt.Stringer for HistogramUpdateType. +func (hut HistogramUpdateType) String() string { + switch hut { + case HistogramUpdateAuto: + return "AUTO UPDATE" + case HistogramUpdateManual: + return "MANUAL UPDATE" + } + return "" +} + // AnalyzeOpt stores the analyze option type and value. type AnalyzeOpt struct { Type AnalyzeOptionType @@ -142,6 +171,10 @@ func (n *AnalyzeTableStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteName(columnName.O) } } + if n.HistogramData != "" { + ctx.WriteKeyWord(" USING DATA ") + ctx.WriteString(n.HistogramData) + } } switch n.ColumnChoice { case AllColumns: @@ -178,6 +211,10 @@ func (n *AnalyzeTableStmt) Restore(ctx *format.RestoreCtx) error { ctx.WritePlain(AnalyzeOptionString[opt.Type]) } } + if n.HistogramUpdate != HistogramUpdateNop { + ctx.WritePlain(" ") + ctx.WriteKeyWord(n.HistogramUpdate.String()) + } return nil } diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 59826ce..4368acc 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -397,6 +397,12 @@ var unReservedKeywordNames = []string{ "CALIBRATE", "WORK", "WORKLOAD", + "AUTO", + "EXPORT", + "OPTIMIZER_COSTS", + "PERSIST_ONLY", + "RELAY", + "USER_RESOURCES", "TPCC", "OLTP_READ_WRITE", "OLTP_READ_ONLY", diff --git a/parser/keywords.go b/parser/keywords.go index ef79d14..790c520 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -289,6 +289,7 @@ var Keywords = []KeywordsType{ {"AT", false, "unreserved"}, {"ATTRIBUTE", false, "unreserved"}, {"ATTRIBUTES", false, "unreserved"}, + {"AUTO", false, "unreserved"}, {"AUTOEXTEND_SIZE", false, "unreserved"}, {"AUTO_ID_CACHE", false, "unreserved"}, {"AUTO_INCREMENT", false, "unreserved"}, @@ -412,6 +413,7 @@ var Keywords = []KeywordsType{ {"EXPANSION", false, "unreserved"}, {"EXPIRE", false, "unreserved"}, {"EXPLORE", false, "unreserved"}, + {"EXPORT", false, "unreserved"}, {"EXTENDED", false, "unreserved"}, {"FAILED_LOGIN_ATTEMPTS", false, "unreserved"}, {"FAST", false, "unreserved"}, @@ -534,6 +536,7 @@ var Keywords = []KeywordsType{ {"ONLY", false, "unreserved"}, {"ON_DUPLICATE", false, "unreserved"}, {"OPEN", false, "unreserved"}, + {"OPTIMIZER_COSTS", false, "unreserved"}, {"OPTIONAL", false, "unreserved"}, {"OPTIONS", false, "unreserved"}, {"PACK_KEYS", false, "unreserved"}, @@ -552,6 +555,7 @@ var Keywords = []KeywordsType{ {"PAUSE", false, "unreserved"}, {"PERCENT", false, "unreserved"}, {"PERSIST", false, "unreserved"}, + {"PERSIST_ONLY", false, "unreserved"}, {"PER_DB", false, "unreserved"}, {"PER_TABLE", false, "unreserved"}, {"PHASE", false, "unreserved"}, @@ -583,6 +587,7 @@ var Keywords = []KeywordsType{ {"RECOVER", false, "unreserved"}, {"REDUNDANT", false, "unreserved"}, {"REFRESH", false, "unreserved"}, + {"RELAY", false, "unreserved"}, {"RELAYLOG", false, "unreserved"}, {"RELOAD", false, "unreserved"}, {"REMOVE", false, "unreserved"}, @@ -714,6 +719,7 @@ var Keywords = []KeywordsType{ {"UNSET", false, "unreserved"}, {"UPGRADE", false, "unreserved"}, {"USER", false, "unreserved"}, + {"USER_RESOURCES", false, "unreserved"}, {"USE_FRM", false, "unreserved"}, {"VALIDATION", false, "unreserved"}, {"VALUE", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index dd4b4ea..836cf62 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(760, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 760) + if !reflect.DeepEqual(766, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 766) } reservedNr := 0 diff --git a/parser/lexer.go b/parser/lexer.go index cf2c76f..84ba60b 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -626,7 +626,7 @@ func startWithAt(s *Scanner) (tok int, pos Pos, lit string) { s.r.inc() stream := s.r.s[pos.Offset+2:] var prefix string - for _, v := range []string{"global.", "session.", "local."} { + for _, v := range []string{"global.", "session.", "local.", "persist.", "persist_only."} { if len(v) > len(stream) { continue } diff --git a/parser/misc.go b/parser/misc.go index a1208f3..dd52e01 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -195,6 +195,7 @@ var tokenMap = map[string]int{ "AUTO_INCREMENT": autoIncrement, "AUTO_RANDOM": autoRandom, "AUTO_RANDOM_BASE": autoRandomBase, + "AUTO": auto, "AUTOEXTEND_SIZE": autoextendSize, "AVG_ROW_LENGTH": avgRowLength, "AVG": avg, @@ -416,6 +417,7 @@ var tokenMap = map[string]int{ "EXPR_PUSHDOWN_BLACKLIST": exprPushdownBlacklist, "EXTENDED": extended, "EXPLORE": explore, + "EXPORT": export, "EXTERNAL": external, "EXTRACT": extract, "FALSE": falseKwd, @@ -659,6 +661,7 @@ var tokenMap = map[string]int{ "OPTIMISTIC": optimistic, "OPTIMIZE": optimize, "OPTION": option, + "OPTIMIZER_COSTS": optimizerCosts, "OPTIONAL": optional, "OPTIONALLY": optionally, "OPTIONS": options, @@ -685,6 +688,7 @@ var tokenMap = map[string]int{ "PER_DB": per_db, "PER_TABLE": per_table, "PERSIST": persist, + "PERSIST_ONLY": persistOnly, "PESSIMISTIC": pessimistic, "PHASE": phase, "PLACEMENT": placement, @@ -740,6 +744,7 @@ var tokenMap = map[string]int{ "REGEXP": regexpKwd, "REGION": region, "REGIONS": regions, + "RELAY": relay, "RELAYLOG": relaylog, "RELEASE": release, "RELOAD": reload, @@ -987,6 +992,7 @@ var tokenMap = map[string]int{ "USAGE": usage, "USE": use, "USER": user, + "USER_RESOURCES": userResources, "USE_FRM": useFrm, "USING": using, "UTC_DATE": utcDate, diff --git a/parser/parse_analyze.go b/parser/parse_analyze.go index f951a14..aabad46 100644 --- a/parser/parse_analyze.go +++ b/parser/parse_analyze.go @@ -117,17 +117,36 @@ func (r *rdParser) parseAnalyzeTableStmt() ast.StmtNode { AnalyzeOpts: r.parseAnalyzeOptionListOpt(), } case update: - // ... "UPDATE" "HISTOGRAM" "ON" IdentList AnalyzeOptionListOpt + // ... "UPDATE" "HISTOGRAM" "ON" IdentList + // ("USING" "DATA" stringLit + // | AnalyzeOptionListOpt [("AUTO"|"MANUAL") "UPDATE"]) r.advance() r.expect(histogram) r.expect(on) - return &ast.AnalyzeTableStmt{ + stmt := &ast.AnalyzeTableStmt{ TableNames: []*ast.TableName{table}, NoWriteToBinLog: noWriteToBinLog, ColumnNames: r.parseIdentList(), - AnalyzeOpts: r.parseAnalyzeOptionListOpt(), HistogramOperation: ast.HistogramOperationUpdate, } + if r.tok() == using { + r.advance() + r.expect(data) + stmt.HistogramData = r.expect(stringLit).lit + return stmt + } + stmt.AnalyzeOpts = r.parseAnalyzeOptionListOpt() + switch r.tok() { + case auto: + r.advance() + r.expect(update) + stmt.HistogramUpdate = ast.HistogramUpdateAuto + case manual: + r.advance() + r.expect(update) + stmt.HistogramUpdate = ast.HistogramUpdateManual + } + return stmt case drop: // ... "DROP" "HISTOGRAM" "ON" IdentList r.advance() diff --git a/parser/parse_drop.go b/parser/parse_drop.go index 724486e..2695662 100644 --- a/parser/parse_drop.go +++ b/parser/parse_drop.go @@ -74,7 +74,7 @@ func (r *rdParser) parseDropStmtFamily() ast.StmtNode { } case resource: // DropResourceGroupStmt: "DROP" "RESOURCE" "GROUP" IfExists - // ResourceGroupName + // ResourceGroupName ["FORCE"] r.advance() r.advance() r.expect(group) @@ -82,6 +82,7 @@ func (r *rdParser) parseDropStmtFamily() ast.StmtNode { return &ast.DropResourceGroupStmt{ IfExists: ifExists, ResourceGroupName: ast.NewCIStr(r.parseResourceGroupName()), + Force: r.accept(force), } case prepare: // DeallocateStmt: DeallocateSym "PREPARE" Identifier, for the diff --git a/parser/parse_misc.go b/parser/parse_misc.go index dedc0d2..a7b1ec6 100644 --- a/parser/parse_misc.go +++ b/parser/parse_misc.go @@ -157,6 +157,12 @@ func (r *rdParser) parseFlushStmt() ast.StmtNode { } st := r.parseFlushOption() st.NoWriteToBinLog = noWrite + // FlushOptionList: a comma continues the list (FLUSH BINARY LOGS, + // STATUS, ...). A FLUSH TABLES table list consumes its own commas, so + // only commas after a complete option arrive here. + for r.accept(int(',')) { + st.MoreOptions = append(st.MoreOptions, r.parseFlushOption()) + } return st } @@ -187,8 +193,8 @@ func (r *rdParser) parseFlushOption() *ast.FlushStmt { Tp: ast.FlushLogs, LogType: ast.LogTypeDefault, } - case binaryType, engine, errorKwd, general, slow: - // LogTypeOpt "LOGS" + case binaryType, engine, errorKwd, general, slow, relay: + // LogTypeOpt "LOGS" [ForChannelOpt (RELAY only)] var logType ast.LogType switch r.tok() { case binaryType: @@ -201,15 +207,27 @@ func (r *rdParser) parseFlushOption() *ast.FlushStmt { logType = ast.LogTypeGeneral case slow: logType = ast.LogTypeSlow + case relay: + logType = ast.LogTypeRelay } r.advance() r.expect(logs) - return &ast.FlushStmt{ + st := &ast.FlushStmt{ Tp: ast.FlushLogs, LogType: logType, } + if logType == ast.LogTypeRelay { + st.Channel = r.parseForChannelOpt() + } + return st + case optimizerCosts: + r.advance() + return &ast.FlushStmt{Tp: ast.FlushOptimizerCosts} + case userResources: + r.advance() + return &ast.FlushStmt{Tp: ast.FlushUserResources} case tableKwd, tables: - // TableOrTables TableNameListOpt WithReadLockOpt + // TableOrTables TableNameListOpt (WithReadLockOpt | "FOR" "EXPORT") r.advance() st := &ast.FlushStmt{ Tp: ast.FlushTables, @@ -222,6 +240,10 @@ func (r *rdParser) parseFlushOption() *ast.FlushStmt { r.expect(read) r.expect(lock) st.ReadLock = true + } else if r.tok() == forKwd && r.la(1) == export { + r.advance() + r.advance() + st.ForExport = true } return st case clientErrorsSummary: diff --git a/parser/parse_set.go b/parser/parse_set.go index 4360303..5c66328 100644 --- a/parser/parse_set.go +++ b/parser/parse_set.go @@ -110,11 +110,18 @@ func (r *rdParser) parseSetStmt() ast.StmtNode { return &ast.SetSessionStatesStmt{SessionStates: t.lit} } case resource: - // "SET" "RESOURCE" "GROUP" ResourceGroupName + // "SET" "RESOURCE" "GROUP" ResourceGroupName ["FOR" NUM (',' NUM)*] if r.la(1) == group { r.advance() r.advance() - return &ast.SetResourceGroupStmt{Name: ast.NewCIStr(r.parseResourceGroupName())} + stmt := &ast.SetResourceGroupStmt{Name: ast.NewCIStr(r.parseResourceGroupName())} + if r.accept(forKwd) { + stmt.ThreadIDs = []int64{r.parseInt64Num()} + for r.accept(int(',')) { + stmt.ThreadIDs = append(stmt.ThreadIDs, r.parseInt64Num()) + } + } + return stmt } } // "SET" VariableAssignmentList @@ -320,6 +327,22 @@ func (r *rdParser) parseVariableAssignment() *ast.VariableAssignment { r.parseEqOrAssignmentEq() return &ast.VariableAssignment{Name: name, Value: r.parseSetExpr(), IsInstance: true, IsSystem: true} } + case persist: + // "PERSIST" VariableName EqOrAssignmentEq SetExpr + if isIdentifierTok(r.la(1)) { + r.advance() + name := r.parseVariableName() + r.parseEqOrAssignmentEq() + return &ast.VariableAssignment{Name: name, Value: r.parseSetExpr(), IsPersist: true, IsGlobal: true, IsSystem: true} + } + case persistOnly: + // "PERSIST_ONLY" VariableName EqOrAssignmentEq SetExpr + if isIdentifierTok(r.la(1)) { + r.advance() + name := r.parseVariableName() + r.parseEqOrAssignmentEq() + return &ast.VariableAssignment{Name: name, Value: r.parseSetExpr(), IsPersistOnly: true, IsGlobal: true, IsSystem: true} + } case session, local: // "SESSION"/"LOCAL" VariableName EqOrAssignmentEq SetExpr if isIdentifierTok(r.la(1)) { @@ -335,12 +358,22 @@ func (r *rdParser) parseVariableAssignment() *ast.VariableAssignment { r.parseEqOrAssignmentEq() var isGlobal bool var isInstance bool + var isPersist bool + var isPersistOnly bool if strings.HasPrefix(v, "@@global.") { isGlobal = true v = strings.TrimPrefix(v, "@@global.") } else if strings.HasPrefix(v, "@@instance.") { isInstance = true v = strings.TrimPrefix(v, "@@instance.") + } else if strings.HasPrefix(v, "@@persist.") { + isPersist = true + isGlobal = true + v = strings.TrimPrefix(v, "@@persist.") + } else if strings.HasPrefix(v, "@@persist_only.") { + isPersistOnly = true + isGlobal = true + v = strings.TrimPrefix(v, "@@persist_only.") } else if strings.HasPrefix(v, "@@session.") { v = strings.TrimPrefix(v, "@@session.") } else if strings.HasPrefix(v, "@@local.") { @@ -348,7 +381,7 @@ func (r *rdParser) parseVariableAssignment() *ast.VariableAssignment { } else if strings.HasPrefix(v, "@@") { v = strings.TrimPrefix(v, "@@") } - return &ast.VariableAssignment{Name: v, Value: r.parseSetExpr(), IsGlobal: isGlobal, IsInstance: isInstance, IsSystem: true} + return &ast.VariableAssignment{Name: v, Value: r.parseSetExpr(), IsGlobal: isGlobal, IsInstance: isInstance, IsPersist: isPersist, IsPersistOnly: isPersistOnly, IsSystem: true} case singleAtIdentifier: // singleAtIdentifier EqOrAssignmentEq Expression v := strings.TrimPrefix(r.cur().lit, "@") diff --git a/parser/testdata/parser/mysql_unsupported_admin/output.sql b/parser/testdata/parser/mysql_unsupported_admin/output.sql index 7144f5e..78b3165 100644 --- a/parser/testdata/parser/mysql_unsupported_admin/output.sql +++ b/parser/testdata/parser/mysql_unsupported_admin/output.sql @@ -1,35 +1,35 @@ --- error: line 1 column 44 near "USING DATA 'json'" +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` USING DATA 'json' -- case --- error: line 1 column 43 near "AUTO UPDATE" +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` AUTO UPDATE -- case --- error: line 1 column 45 near "MANUAL UPDATE" +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` MANUAL UPDATE -- case --- error: line 1 column 59 near "AUTO UPDATE" +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` WITH 20 BUCKETS AUTO UPDATE -- case --- error: line 1 column 43 near "v1 = 1" +INSTALL COMPONENT 'file://c' SET @@PERSIST.`v1`=1 -- case --- error: line 1 column 28 near "FORCE" +DROP RESOURCE GROUP `rg` FORCE -- case --- error: line 1 column 25 near "FOR 4" +SET RESOURCE GROUP `rg` FOR 4 -- case --- error: line 1 column 25 near "FOR 4, 5, 6" +SET RESOURCE GROUP `rg` FOR 4, 5, 6 -- case --- error: line 1 column 21 near "OPTIMIZER_COSTS" +FLUSH OPTIMIZER_COSTS -- case --- error: line 1 column 11 near "RELAY LOGS" +FLUSH RELAY LOGS -- case --- error: line 1 column 11 near "RELAY LOGS FOR CHANNEL 'ch'" +FLUSH RELAY LOGS FOR CHANNEL 'ch' -- case --- error: line 1 column 20 near "USER_RESOURCES" +FLUSH USER_RESOURCES -- case --- error: line 1 column 23 near "FOR EXPORT" +FLUSH TABLES `t1`, `t2` FOR EXPORT -- case --- error: line 1 column 18 near ", ERROR LOGS, STATUS" +FLUSH BINARY LOGS, ERROR LOGS, STATUS -- case --- error: line 1 column 27 near "max_connections = 200" +SET @@PERSIST.`max_connections`=200 -- case --- error: line 1 column 32 near "max_connections = 250" +SET @@PERSIST_ONLY.`max_connections`=250 -- case --- error: line 1 column 27 near "max_connections = 200, long_query_time = 0.5" +SET @@PERSIST.`max_connections`=200, @@SESSION.`long_query_time`=0.5 -- case --- error: line 1 column 57 near "long_query_time = 1" +SET @@GLOBAL.`max_connections`=200, @@PERSIST.`long_query_time`=1 diff --git a/parser/token_kinds.go b/parser/token_kinds.go index ab8f15f..24f2588 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -1026,6 +1026,12 @@ const ( without = 57990 work = 58328 workload = 57992 + auto = 58329 + export = 58330 + optimizerCosts = 58331 + persistOnly = 58332 + relay = 58333 + userResources = 58334 wrapper = 58319 write = 57592 x509 = 57993 From e145e8e521a97dabeedd4249a4a98249733e609f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:18:43 +0000 Subject: [PATCH 07/13] Support column INVISIBLE/VISIBLE, column ENGINE_ATTRIBUTE, and DEFAULT (expr) with operators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_types coverage group (MySQL 26.7 §13.1.20, §15.1.9): its error goldens turn into Restore() goldens. - VISIBLE and INVISIBLE parse as column attributes (new ColumnOptionVisible/ColumnOptionInvisible), covering CREATE TABLE, ADD/MODIFY COLUMN, and the GIPK SHOW CREATE TABLE round-trip through the /*!80023 ... */ versioned comment. - ALTER TABLE ... ALTER COLUMN col SET {VISIBLE|INVISIBLE} parses as a new AlterTableAlterColumnVisibility spec, reusing the existing AlterTableSpec.Visibility field. - ENGINE_ATTRIBUTE joins SECONDARY_ENGINE_ATTRIBUTE as a column attribute (new ColumnOptionEngineAttribute), with and without =. - DEFAULT (expr): when the goyacc-shaped '('-led DefaultValueExpr alternatives do not span the parentheses, the content reparses as a full parenthesized expression, so operators and INTERVAL arithmetic parse. BinaryOperationExpr defaults restore inside parentheses, which MySQL requires; other shapes keep their existing Restore() output. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/ddl.go | 31 ++++++ parser/parse_alter.go | 15 ++- parser/parse_column.go | 94 ++++++++++++------- .../parser/mysql_unsupported_types/output.sql | 24 ++--- 4 files changed, 117 insertions(+), 47 deletions(-) diff --git a/ast/ddl.go b/ast/ddl.go index 7a93caa..2ba738e 100644 --- a/ast/ddl.go +++ b/ast/ddl.go @@ -533,6 +533,9 @@ const ( ColumnOptionAutoRandom ColumnOptionSecondaryEngineAttribute ColumnOptionSrid + ColumnOptionVisible + ColumnOptionInvisible + ColumnOptionEngineAttribute ) var ( @@ -604,6 +607,11 @@ func (n *ColumnOption) Restore(ctx *format.RestoreCtx) error { if _, ok := n.Expr.(*ColumnNameExpr); ok { printOuterParentheses = true } + if _, ok := n.Expr.(*BinaryOperationExpr); ok { + // DEFAULT (expr) with an operator; MySQL requires the + // parentheses. + printOuterParentheses = true + } if printOuterParentheses { ctx.WritePlain("(") } @@ -697,6 +705,14 @@ func (n *ColumnOption) Restore(ctx *format.RestoreCtx) error { case ColumnOptionSrid: ctx.WriteKeyWord("SRID ") ctx.WritePlainf("%d", n.UintValue) + case ColumnOptionVisible: + ctx.WriteKeyWord("VISIBLE") + case ColumnOptionInvisible: + ctx.WriteKeyWord("INVISIBLE") + case ColumnOptionEngineAttribute: + ctx.WriteKeyWord("ENGINE_ATTRIBUTE") + ctx.WritePlain(" = ") + ctx.WriteString(n.StrValue) default: return errors.New("An error occurred while splicing ColumnOption") } @@ -3564,6 +3580,11 @@ const ( AlterTableDropMaskingPolicy AlterTableModifyMaskingPolicyExpression AlterTableModifyMaskingPolicyRestrictOn + // AlterTableAlterColumnVisibility is + // ALTER TABLE ... ALTER COLUMN col SET {VISIBLE | INVISIBLE}; + // the column is NewColumns[0] (name only) and the Visibility field + // carries the choice. + AlterTableAlterColumnVisibility ) // LockType is the type for AlterTableSpec. @@ -3964,6 +3985,16 @@ func (n *AlterTableSpec) Restore(ctx *format.RestoreCtx) error { } else { ctx.WriteKeyWord(" DROP DEFAULT") } + case AlterTableAlterColumnVisibility: + ctx.WriteKeyWord("ALTER COLUMN ") + if err := n.NewColumns[0].Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterTableSpec.NewColumns[0]") + } + if n.Visibility == IndexVisibilityInvisible { + ctx.WriteKeyWord(" SET INVISIBLE") + } else { + ctx.WriteKeyWord(" SET VISIBLE") + } case AlterTableLock: ctx.WriteKeyWord("LOCK ") ctx.WritePlain("= ") diff --git a/parser/parse_alter.go b/parser/parse_alter.go index ef9cace..7654a6c 100644 --- a/parser/parse_alter.go +++ b/parser/parse_alter.go @@ -1136,12 +1136,25 @@ func (r *rdParser) parseAlterTableSpecAlter() *ast.AlterTableSpec { } } // "ALTER" ColumnKeywordOpt ColumnName - // ("SET" "DEFAULT" (SignedLiteral | '(' Expression ')') | "DROP" "DEFAULT") + // ("SET" "DEFAULT" (SignedLiteral | '(' Expression ')') + // | "SET" ("VISIBLE" | "INVISIBLE") | "DROP" "DEFAULT") r.accept(column) col := r.parseColumnName() switch r.tok() { case set: r.advance() + if r.tok() == visible || r.tok() == invisible { + visibility := ast.IndexVisibilityVisible + if r.tok() == invisible { + visibility = ast.IndexVisibilityInvisible + } + r.advance() + return &ast.AlterTableSpec{ + Tp: ast.AlterTableAlterColumnVisibility, + NewColumns: []*ast.ColumnDef{{Name: col}}, + Visibility: visibility, + } + } r.expect(defaultKwd) var expr ast.ExprNode if r.accept(int('(')) { diff --git a/parser/parse_column.go b/parser/parse_column.go index 5b2cbb9..bd461c3 100644 --- a/parser/parse_column.go +++ b/parser/parse_column.go @@ -59,7 +59,7 @@ func (r *rdParser) isColumnOptionStart() bool { case not, not2, null, autoIncrement, primary, key, unique, defaultKwd, serial, on, comment, check, constraint, generated, as, references, collate, columnFormat, storage, autoRandom, secondaryEngineAttribute, - srid: + srid, visible, invisible, engine_attribute: return true } return false @@ -263,6 +263,22 @@ func (r *rdParser) parseColumnOption() interface{} { Tp: ast.ColumnOptionSecondaryEngineAttribute, StrValue: r.expect(stringLit).lit, } + case engine_attribute: + // ColumnOption: "ENGINE_ATTRIBUTE" EqOpt stringLit + r.advance() + r.parseEqOpt() + return &ast.ColumnOption{ + Tp: ast.ColumnOptionEngineAttribute, + StrValue: r.expect(stringLit).lit, + } + case visible: + // ColumnOption: "VISIBLE" + r.advance() + return &ast.ColumnOption{Tp: ast.ColumnOptionVisible} + case invisible: + // ColumnOption: "INVISIBLE" + r.advance() + return &ast.ColumnOption{Tp: ast.ColumnOptionInvisible} case srid: // ColumnOption: "SRID" LengthNum — the spatial column attribute // (MySQL 26.7 §13.1.20.10); postdates the goyacc grammar. @@ -423,45 +439,55 @@ func (r *rdParser) parseDefaultValueExpr() ast.ExprNode { // alternatives: '(' Identifier ')' and '(' SignedLiteral ')' (one level // only), plus the parenthesized recursions of BuiltinFunction, // NowSymOptionFractionParentheses, and NextValueForSequenceParentheses. +// When none of those alternatives spans the parentheses, the content +// reparses as a full '(' Expression ')' — the MySQL DEFAULT (expr) form +// with operators. func (r *rdParser) parseDefaultValueExprParen() ast.ExprNode { start := r.cur().offset - r.expect(int('(')) var v ast.ExprNode - switch r.tok() { - case currentTs, localTime, localTs, builtinNow, builtinCurDate, currentDate: - v = r.parseNowSymOptionFraction() - case next: - // NEXT can also be an Identifier; "VALUE" commits to - // NextValueForSequence the way the LALR states do. - if r.la(1) == value { - v = r.parseNextValueForSequence() - } else { - v = r.parseParenIdentifierExpr() - } - case nextval: - if r.la(1) == int('(') { - v = r.parseNextValueForSequence() - } else { - v = r.parseParenIdentifierExpr() - } - case identifier: - if r.la(1) == int('(') { + if ok := r.try(func() { + r.expect(int('(')) + switch r.tok() { + case currentTs, localTime, localTs, builtinNow, builtinCurDate, currentDate: + v = r.parseNowSymOptionFraction() + case next: + // NEXT can also be an Identifier; "VALUE" commits to + // NextValueForSequence the way the LALR states do. + if r.la(1) == value { + v = r.parseNextValueForSequence() + } else { + v = r.parseParenIdentifierExpr() + } + case nextval: + if r.la(1) == int('(') { + v = r.parseNextValueForSequence() + } else { + v = r.parseParenIdentifierExpr() + } + case identifier: + if r.la(1) == int('(') { + v = r.parseBuiltinFunction() + } else { + v = r.parseParenIdentifierExpr() + } + case replace: v = r.parseBuiltinFunction() - } else { - v = r.parseParenIdentifierExpr() - } - case replace: - v = r.parseBuiltinFunction() - case int('('): - v = r.parseDefaultNestedParen() - default: - if isIdentifierTok(r.tok()) { - v = r.parseParenIdentifierExpr() - } else { - v = r.parseSignedLiteral() + case int('('): + v = r.parseDefaultNestedParen() + default: + if isIdentifierTok(r.tok()) { + v = r.parseParenIdentifierExpr() + } else { + v = r.parseSignedLiteral() + } } + r.expect(int(')')) + }); !ok { + // '(' Expression ')' + r.expect(int('(')) + v = r.parseExpression() + r.expect(int(')')) } - r.expect(int(')')) return r.setOrigin(v, start) } diff --git a/parser/testdata/parser/mysql_unsupported_types/output.sql b/parser/testdata/parser/mysql_unsupported_types/output.sql index 1f3696d..06e88c0 100644 --- a/parser/testdata/parser/mysql_unsupported_types/output.sql +++ b/parser/testdata/parser/mysql_unsupported_types/output.sql @@ -1,23 +1,23 @@ --- error: line 1 column 31 near "INVISIBLE)" +CREATE TABLE `t` (`a` INT INVISIBLE) -- case --- error: line 1 column 29 near "VISIBLE)" +CREATE TABLE `t` (`a` INT VISIBLE) -- case --- error: line 1 column 50 near "INVISIBLE)" +CREATE TABLE `t` (`a` INT NOT NULL DEFAULT 5 INVISIBLE) -- case --- error: line 1 column 93 near "INVISIBLE */, `col1` int DEFAULT NULL, PRIMARY KEY (`my_row_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci" +CREATE TABLE `gipk_t` (`my_row_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT INVISIBLE,`col1` INT DEFAULT NULL,PRIMARY KEY(`my_row_id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_0900_AI_CI -- case --- error: line 1 column 40 near "INVISIBLE" +ALTER TABLE `t` ADD COLUMN `a` INT INVISIBLE -- case --- error: line 1 column 40 near "VISIBLE" +ALTER TABLE `t` ALTER COLUMN `a` SET VISIBLE -- case --- error: line 1 column 42 near "INVISIBLE" +ALTER TABLE `t` ALTER COLUMN `a` SET INVISIBLE -- case --- error: line 1 column 41 near "VISIBLE" +ALTER TABLE `t` MODIFY COLUMN `a` INT VISIBLE -- case --- error: line 1 column 38 near "ENGINE_ATTRIBUTE = '{"k": 1}' SECONDARY_ENGINE_ATTRIBUTE = '{"k": 2}')" +CREATE TABLE `t` (`a` INT ENGINE_ATTRIBUTE = '{"k": 1}' SECONDARY_ENGINE_ATTRIBUTE = '{"k": 2}') -- case --- error: line 1 column 38 near "ENGINE_ATTRIBUTE '{"k": 1}')" +CREATE TABLE `t` (`a` INT ENGINE_ATTRIBUTE = '{"k": 1}') -- case --- error: line 1 column 34 near "+ 1))" +CREATE TABLE `t` (`a` INT DEFAULT (1+1)) -- case --- error: line 1 column 46 near "+ INTERVAL 1 YEAR))" +CREATE TABLE `t` (`a` DATE DEFAULT (DATE_ADD(CURRENT_DATE(), INTERVAL 1 YEAR))) From 594955af8b10e0c4865be5f4de283c9f55abaa45 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:24:40 +0000 Subject: [PATCH 08/13] Support DECLARE ... CONDITION and the LOOP statement in stored programs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_compound coverage group (MySQL 26.7 §15.6.5, §15.6.7.1): its error goldens turn into Restore() goldens. - DECLARE name CONDITION FOR {SQLSTATE [VALUE] 'x' | mysql_error_code} parses as a new ProcedureConditionDecl declaration, and handler condition lists accept a declared condition name (new ProcedureErrorName ErrNode). - [label:] LOOP ... END LOOP [label] parses as a new ProcedureLoopStmt, joining WHILE/REPEAT in the unlabeled and labeled loop productions. Keyword tables: CONDITION and LOOP become reserved words, matching their MySQL 26.7 classification; GET DIAGNOSTICS ... CONDITION now matches the token instead of an identifier spelling. TestKeywordsLength counts updated. testdata/errors.json is unaffected. ProcedureJump (LEAVE/ITERATE) restored its label as a quoted string literal, which does not re-parse; it now restores as an identifier. The test-only nodeTextCleaner also learns to clean the statements of a ProcedureBlock, whose Accept deliberately does not traverse them, so procedure bodies compare deep-equal in the round-trip harness. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/procedure.go | 101 +++++++++++++++++- ast/sem.go | 10 ++ parser/keywords.go | 2 + parser/keywords_test.go | 8 +- parser/misc.go | 2 + parser/parse_procedure.go | 38 ++++++- parser/parse_signal.go | 6 +- parser/parser_test.go | 8 ++ .../mysql_unsupported_compound/output.sql | 10 +- parser/token_kinds.go | 2 + 10 files changed, 171 insertions(+), 16 deletions(-) diff --git a/ast/procedure.go b/ast/procedure.go index 6084950..8fcdaec 100644 --- a/ast/procedure.go +++ b/ast/procedure.go @@ -795,6 +795,45 @@ func (n *ProcedureWhileStmt) Accept(v Visitor) (Node, bool) { return v.Leave(n) } +// ProcedureLoopStmt stores `LOOP ... END LOOP` statement. +type ProcedureLoopStmt struct { + stmtNode + + Body []StmtNode +} + +// Restore implements ProcedureLoopStmt interface. +func (n *ProcedureLoopStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("LOOP ") + for _, stmt := range n.Body { + err := stmt.Restore(ctx) + if err != nil { + return err + } + ctx.WriteKeyWord(";") + } + ctx.WriteKeyWord("END LOOP") + return nil +} + +// Accept implements ProcedureLoopStmt Accept interface. +func (n *ProcedureLoopStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ProcedureLoopStmt) + + for i, stmt := range n.Body { + node, ok := stmt.Accept(v) + if !ok { + return n, false + } + n.Body[i] = node.(StmtNode) + } + return v.Leave(n) +} + // ProcedureCursor stores procedure cursor statement. type ProcedureCursor struct { ProcedureDeclInfo @@ -1005,6 +1044,66 @@ func (n *ProcedureErrorState) Accept(v Visitor) (Node, bool) { return v.Leave(n) } +// ProcedureErrorName references a condition declared with +// DECLARE ... CONDITION in a handler condition list. +type ProcedureErrorName struct { + ProcedureErrorCondition + + Name string +} + +// Restore implements ProcedureErrorName interface. +func (n *ProcedureErrorName) Restore(ctx *format.RestoreCtx) error { + ctx.WriteName(n.Name) + return nil +} + +// Accept implements ProcedureErrorName Accept interface. +func (n *ProcedureErrorName) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ProcedureErrorName) + return v.Leave(n) +} + +// ProcedureConditionDecl stores a DECLARE ... CONDITION FOR declaration; +// Value is the named condition's value, a ProcedureErrorVal (a MySQL +// error number) or ProcedureErrorState (an SQLSTATE value). +type ProcedureConditionDecl struct { + ProcedureDeclInfo + + Name string + Value ErrNode +} + +// Restore implements ProcedureConditionDecl interface. +func (n *ProcedureConditionDecl) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DECLARE ") + ctx.WriteName(n.Name) + ctx.WriteKeyWord(" CONDITION FOR ") + if err := n.Value.Restore(ctx); err != nil { + return annotate(err, "An error occur while restore ProcedureConditionDecl.Value") + } + return nil +} + +// Accept implements ProcedureConditionDecl Accept interface. +func (n *ProcedureConditionDecl) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*ProcedureConditionDecl) + node, ok := n.Value.Accept(v) + if !ok { + return n, false + } + n.Value = node.(ErrNode) + return v.Leave(n) +} + // ProcedureErrorCon stores procedure handler status info. type ProcedureErrorCon struct { ProcedureErrorCondition @@ -1174,7 +1273,7 @@ func (n *ProcedureJump) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("ITERATE ") } - ctx.WriteString(n.Name) + ctx.WriteName(n.Name) return nil } diff --git a/ast/sem.go b/ast/sem.go index 4152927..32657da 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -1481,6 +1481,16 @@ func (n *ProcedureWhileStmt) SEMCommand() string { return ProcedureCommand } +// SEMCommand returns the command string for the statement. +func (n *ProcedureLoopStmt) SEMCommand() string { + return ProcedureCommand +} + +// SEMCommand returns the command string for the statement. +func (n *ProcedureErrorName) SEMCommand() string { + return ProcedureCommand +} + // SEMCommand returns the command string for the statement. func (n *ProcedureOpenCur) SEMCommand() string { return ProcedureCommand diff --git a/parser/keywords.go b/parser/keywords.go index 790c520..a65db78 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -50,6 +50,7 @@ var Keywords = []KeywordsType{ {"CHECK", true, "reserved"}, {"COLLATE", true, "reserved"}, {"COLUMN", true, "reserved"}, + {"CONDITION", true, "reserved"}, {"CONSTRAINT", true, "reserved"}, {"CONTINUE", true, "reserved"}, {"CONVERT", true, "reserved"}, @@ -157,6 +158,7 @@ var Keywords = []KeywordsType{ {"LONG", true, "reserved"}, {"LONGBLOB", true, "reserved"}, {"LONGTEXT", true, "reserved"}, + {"LOOP", true, "reserved"}, {"LOW_PRIORITY", true, "reserved"}, {"MATCH", true, "reserved"}, {"MAXVALUE", true, "reserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 836cf62..a8f76ac 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(766, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 766) + if !reflect.DeepEqual(768, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 768) } reservedNr := 0 @@ -53,8 +53,8 @@ func TestKeywordsLength(t *testing.T) { reservedNr += 1 } } - if !reflect.DeepEqual(246, reservedNr) { - t.Fatalf("got %v, want %v", reservedNr, 246) + if !reflect.DeepEqual(248, reservedNr) { + t.Fatalf("got %v, want %v", reservedNr, 248) } } diff --git a/parser/misc.go b/parser/misc.go index dd52e01..435bd76 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -289,6 +289,7 @@ var tokenMap = map[string]int{ "CONSTRAINTS": constraints, "CONTAINS": contains, "CONTEXT": context, + "CONDITION": condition, "CONTINUE": continueKwd, "CONVERT": convert, "COOLDOWN": cooldown, @@ -577,6 +578,7 @@ var tokenMap = map[string]int{ "LONG": long, "LONGBLOB": longblobType, "LONGTEXT": longtextType, + "LOOP": loop, "LOW_PRIORITY": lowPriority, "MANUAL": manual, "MASTER": master, diff --git a/parser/parse_procedure.go b/parser/parse_procedure.go index 79437ef..0fbe0a1 100644 --- a/parser/parse_procedure.go +++ b/parser/parse_procedure.go @@ -173,7 +173,7 @@ func (r *rdParser) parseProcedureProcStmt() ast.StmtNode { return r.parseProcedureIfstmt() case caseKwd: return r.parseProcedureCaseStmt() - case while, repeat: + case while, repeat, loop: // ProcedureUnlabelLoopBlock: ProcedureUnlabelLoopStmt return r.parseProcedureUnlabelLoopStmt() case open: @@ -325,6 +325,25 @@ func (r *rdParser) parseProcedureDecl() ast.DeclNode { CurName: name, Selectstring: r.parseProcedureCursorSelectStmt(), } + case r.tok() == identifier && r.la(1) == condition: + // "DECLARE" identifier "CONDITION" "FOR" ProcedurceCond + name := strings.ToLower(r.expect(identifier).lit) + r.expect(condition) + r.expect(forKwd) + decl := &ast.ProcedureConditionDecl{Name: name} + if r.tok() == intLit { + decl.Value = &ast.ProcedureErrorVal{ + ErrorNum: getUint64FromNUM(r.expect(intLit).item), + } + } else { + r.expect(sqlstate) + // optValue: empty | "VALUE" + r.accept(value) + decl.Value = &ast.ProcedureErrorState{ + CodeStatus: r.expect(stringLit).lit, + } + } + return decl } // ProcedureDeclIdents: Identifier (',' Identifier)* names := []string{strings.ToLower(r.parseIdentifier())} @@ -400,6 +419,11 @@ func (r *rdParser) parseProcedureHcond() ast.ErrNode { return &ast.ProcedureErrorCon{ ErrorCon: ast.PROCEDUR_SQLEXCEPTION, } + case identifier: + // A condition name declared with DECLARE ... CONDITION. + return &ast.ProcedureErrorName{ + Name: strings.ToLower(r.expect(identifier).lit), + } } r.syntaxError() return nil @@ -554,6 +578,16 @@ func (r *rdParser) parseProcedureUnlabelLoopStmt() ast.StmtNode { Body: body, } } + if r.tok() == loop { + // "LOOP" ProcedureProcStmt1s "END" "LOOP" + r.advance() + body := r.parseProcedureProcStmt1s() + r.expect(end) + r.expect(loop) + return &ast.ProcedureLoopStmt{ + Body: body, + } + } r.expect(repeat) body := r.parseProcedureProcStmt1s() r.expect(until) @@ -585,7 +619,7 @@ func (r *rdParser) parseProcedureLabeled() ast.StmtNode { labelBlock.LabelEnd = endLabel } return labelBlock - case while, repeat: + case while, repeat, loop: labelLoop := &ast.ProcedureLabelLoop{ LabelName: label, Block: r.parseProcedureUnlabelLoopStmt(), diff --git a/parser/parse_signal.go b/parser/parse_signal.go index daf5de4..82291d0 100644 --- a/parser/parse_signal.go +++ b/parser/parse_signal.go @@ -163,9 +163,7 @@ func (r *rdParser) parseSignalAllowedExpr() ast.ExprNode { // // 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. +// conditionInfoItemNames. func (r *rdParser) parseGetDiagnosticsStmt() ast.StmtNode { r.expect(get) x := &ast.GetDiagnosticsStmt{} @@ -179,7 +177,7 @@ func (r *rdParser) parseGetDiagnosticsStmt() ast.StmtNode { } r.expect(diagnostics) names := statementInfoItemNames - if isIdentifierTok(r.tok()) && strings.EqualFold(r.cur().lit, "CONDITION") { + if r.tok() == condition { r.advance() x.ConditionNumber = r.parseSignalAllowedExpr() names = conditionInfoItemNames diff --git a/parser/parser_test.go b/parser/parser_test.go index 8ae768d..b88b7b1 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -3864,6 +3864,14 @@ func (checker *nodeTextCleaner) Enter(in ast.Node) (out ast.Node, skipChildren b node.Tp.CleanElemIsBinaryLit() case *ast.PartitionOptions: cleanPartition(node) + case *ast.ProcedureBlock: + // ProcedureBlock.Accept deliberately does not traverse + // ProcedureProcStmts; clean them explicitly so restored + // procedure bodies compare deep-equal. + var tmpCleaner nodeTextCleaner + for _, stmt := range node.ProcedureProcStmts { + stmt.Accept(&tmpCleaner) + } } return in, false } diff --git a/parser/testdata/parser/mysql_unsupported_compound/output.sql b/parser/testdata/parser/mysql_unsupported_compound/output.sql index 6e1148b..8d205b6 100644 --- a/parser/testdata/parser/mysql_unsupported_compound/output.sql +++ b/parser/testdata/parser/mysql_unsupported_compound/output.sql @@ -1,9 +1,9 @@ --- error: line 1 column 47 near "CONDITION FOR SQLSTATE '23000'; DECLARE EXIT HANDLER FOR e ROLLBACK; END" +CREATE PROCEDURE `p`() BEGIN DECLARE `e` CONDITION FOR SQLSTATE '23000';DECLARE EXIT HANDLER FOR `e` ROLLBACK; END -- case --- error: line 1 column 47 near "CONDITION FOR 1051; END" +CREATE PROCEDURE `p`() BEGIN DECLARE `e` CONDITION FOR 1051; END -- case --- error: line 1 column 26 near "LOOP SET @x = 1; END LOOP" +CREATE PROCEDURE `p`() LOOP SET @`x`=1;END LOOP -- case --- error: line 1 column 32 near "LOOP SET @x = 1; END LOOP; END" +CREATE PROCEDURE `p`() BEGIN LOOP SET @`x`=1;END LOOP; END -- case --- error: line 1 column 31 near "LOOP LEAVE lbl; END LOOP lbl" +CREATE PROCEDURE `p`() `lbl`: LOOP LEAVE `lbl`;END LOOP `lbl` diff --git a/parser/token_kinds.go b/parser/token_kinds.go index 24f2588..97b3d3a 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -1032,6 +1032,8 @@ const ( persistOnly = 58332 relay = 58333 userResources = 58334 + loop = 58335 + condition = 58336 wrapper = 58319 write = 57592 x509 = 57993 From c64ae357e7d6999228c860c4afe7e53e26034be8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:27:51 +0000 Subject: [PATCH 09/13] Support OUTFILE CHARACTER SET, multiple locking clauses, INTO placement, derived-table column lists, and LOAD DATA options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_dml coverage group (MySQL 26.7 §15.2.13, §15.2.9): its error goldens turn into Restore() goldens. - SELECT ... INTO OUTFILE accepts CHARACTER SET (new SelectIntoOption.Charset field), in both the mid-statement and trailing INTO positions. - A SELECT may carry several locking clauses (FOR SHARE OF t1 NOWAIT FOR UPDATE OF t2 SKIP LOCKED): the first stays in SelectStmt.LockInfo and the rest land in the new MoreLockInfos field; the lock rendering moved into a restoreSelectLockInfo helper shared by both. - The SelectStmtIntoOption also parses between the limit and locking clauses (SELECT ... INTO @a FOR UPDATE); Restore() keeps the trailing position, which MySQL also accepts. - Derived tables accept the column alias list ((VALUES ...) AS v (c1, c2)), filling the existing TableSource.ColumnNames field that only LATERAL tables populated. - LOAD DATA gains CONCURRENT (new field, mirroring LoadXMLStmt), PARTITION (p, ...) (new Partitions field), and IGNORE n ROWS (canonicalized to LINES like LOAD XML). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/dml.go | 123 +++++++++++------- parser/parse_dml.go | 20 ++- parser/parse_select.go | 25 +++- .../parser/mysql_unsupported_dml/output.sql | 16 +-- 4 files changed, 128 insertions(+), 56 deletions(-) diff --git a/ast/dml.go b/ast/dml.go index 58ecc68..6cc2369 100644 --- a/ast/dml.go +++ b/ast/dml.go @@ -1256,6 +1256,9 @@ type SelectStmt struct { Limit *Limit // LockInfo is the lock type LockInfo *SelectLockInfo + // MoreLockInfos are the second and later locking clauses of a + // statement with several (FOR SHARE OF t1 ... FOR UPDATE OF t2 ...). + MoreLockInfos []*SelectLockInfo // TableHints represents the table level Optimizer Hint for join type TableHints []*TableOptimizerHint // IsInBraces indicates whether it's a stmt in brace. @@ -1477,50 +1480,10 @@ func (n *SelectStmt) Restore(ctx *format.RestoreCtx) error { if n.LockInfo != nil { ctx.WritePlain(" ") - switch n.LockInfo.LockType { - case SelectLockNone: - case SelectLockForUpdateNoWait: - ctx.WriteKeyWord("for update") - if len(n.LockInfo.Tables) != 0 { - ctx.WriteKeyWord(" OF ") - restoreTables(ctx, n.LockInfo.Tables) - } - ctx.WriteKeyWord(" nowait") - case SelectLockForUpdateWaitN: - ctx.WriteKeyWord("for update") - if len(n.LockInfo.Tables) != 0 { - ctx.WriteKeyWord(" OF ") - restoreTables(ctx, n.LockInfo.Tables) - } - ctx.WriteKeyWord(" wait") - ctx.WritePlainf(" %d", n.LockInfo.WaitSec) - case SelectLockForShareNoWait: - ctx.WriteKeyWord("for share") - if len(n.LockInfo.Tables) != 0 { - ctx.WriteKeyWord(" OF ") - restoreTables(ctx, n.LockInfo.Tables) - } - ctx.WriteKeyWord(" nowait") - case SelectLockForUpdateSkipLocked: - ctx.WriteKeyWord("for update") - if len(n.LockInfo.Tables) != 0 { - ctx.WriteKeyWord(" OF ") - restoreTables(ctx, n.LockInfo.Tables) - } - ctx.WriteKeyWord(" skip locked") - case SelectLockForShareSkipLocked: - ctx.WriteKeyWord("for share") - if len(n.LockInfo.Tables) != 0 { - ctx.WriteKeyWord(" OF ") - restoreTables(ctx, n.LockInfo.Tables) - } - ctx.WriteKeyWord(" skip locked") - default: - ctx.WriteKeyWord(n.LockInfo.LockType.String()) - if len(n.LockInfo.Tables) != 0 { - ctx.WriteKeyWord(" OF ") - restoreTables(ctx, n.LockInfo.Tables) - } + restoreSelectLockInfo(ctx, n.LockInfo) + for _, li := range n.MoreLockInfos { + ctx.WritePlain(" ") + restoreSelectLockInfo(ctx, li) } } @@ -1533,6 +1496,55 @@ func (n *SelectStmt) Restore(ctx *format.RestoreCtx) error { return nil } +// restoreSelectLockInfo writes one locking clause. +func restoreSelectLockInfo(ctx *format.RestoreCtx, li *SelectLockInfo) { + switch li.LockType { + case SelectLockNone: + case SelectLockForUpdateNoWait: + ctx.WriteKeyWord("for update") + if len(li.Tables) != 0 { + ctx.WriteKeyWord(" OF ") + restoreTables(ctx, li.Tables) + } + ctx.WriteKeyWord(" nowait") + case SelectLockForUpdateWaitN: + ctx.WriteKeyWord("for update") + if len(li.Tables) != 0 { + ctx.WriteKeyWord(" OF ") + restoreTables(ctx, li.Tables) + } + ctx.WriteKeyWord(" wait") + ctx.WritePlainf(" %d", li.WaitSec) + case SelectLockForShareNoWait: + ctx.WriteKeyWord("for share") + if len(li.Tables) != 0 { + ctx.WriteKeyWord(" OF ") + restoreTables(ctx, li.Tables) + } + ctx.WriteKeyWord(" nowait") + case SelectLockForUpdateSkipLocked: + ctx.WriteKeyWord("for update") + if len(li.Tables) != 0 { + ctx.WriteKeyWord(" OF ") + restoreTables(ctx, li.Tables) + } + ctx.WriteKeyWord(" skip locked") + case SelectLockForShareSkipLocked: + ctx.WriteKeyWord("for share") + if len(li.Tables) != 0 { + ctx.WriteKeyWord(" OF ") + restoreTables(ctx, li.Tables) + } + ctx.WriteKeyWord(" skip locked") + default: + ctx.WriteKeyWord(li.LockType.String()) + if len(li.Tables) != 0 { + ctx.WriteKeyWord(" OF ") + restoreTables(ctx, li.Tables) + } + } +} + func restoreTables(ctx *format.RestoreCtx, ts []*TableName) error { for i, v := range ts { if err := v.Restore(ctx); err != nil { @@ -1975,11 +1987,13 @@ type LoadDataStmt struct { dmlNode LowPriority bool + Concurrent bool FileLocRef FileLocRefTp Path string Format *string OnDuplicate OnDuplicateKeyHandlingType Table *TableName + Partitions []CIStr // PARTITION (p, ...); empty when absent Charset *string Columns []*ColumnName FieldsInfo *FieldsClause @@ -1997,6 +2011,9 @@ func (n *LoadDataStmt) Restore(ctx *format.RestoreCtx) error { if n.LowPriority { ctx.WriteKeyWord("LOW_PRIORITY ") } + if n.Concurrent { + ctx.WriteKeyWord("CONCURRENT ") + } switch n.FileLocRef { case FileLocServerOrRemote: case FileLocClient: @@ -2017,6 +2034,17 @@ func (n *LoadDataStmt) Restore(ctx *format.RestoreCtx) error { if err := n.Table.Restore(ctx); err != nil { return annotate(err, "An error occurred while restore LoadDataStmt.Table") } + if len(n.Partitions) > 0 { + ctx.WriteKeyWord(" PARTITION ") + ctx.WritePlain("(") + for i, p := range n.Partitions { + if i != 0 { + ctx.WritePlain(", ") + } + ctx.WriteName(p.O) + } + ctx.WritePlain(")") + } if n.Charset != nil { ctx.WriteKeyWord(" CHARACTER SET ") ctx.WritePlain(*n.Charset) @@ -3915,6 +3943,9 @@ type SelectIntoOption struct { FileName string FieldsInfo *FieldsClause LinesInfo *LinesClause + // Charset is the CHARACTER SET of the OUTFILE form; empty when + // absent. + Charset string // Vars is the variable list of the SelectIntoVars form: user // variables and, in stored programs, program variables (restored as // plain names). @@ -3946,6 +3977,10 @@ func (n *SelectIntoOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("INTO OUTFILE ") ctx.WriteString(n.FileName) + if n.Charset != "" { + ctx.WriteKeyWord(" CHARACTER SET ") + ctx.WritePlain(n.Charset) + } if n.FieldsInfo != nil { if err := n.FieldsInfo.Restore(ctx); err != nil { return annotate(err, "An error occurred while restore SelectInto.FieldsInfo") diff --git a/parser/parse_dml.go b/parser/parse_dml.go index ea515df..29c1c66 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -377,7 +377,14 @@ func (r *rdParser) parseLoadDataStmt() ast.StmtNode { r.expect(load) r.expect(data) x := &ast.LoadDataStmt{FileLocRef: ast.FileLocServerOrRemote} - x.LowPriority = r.accept(lowPriority) + switch r.tok() { + case lowPriority: + r.advance() + x.LowPriority = true + case concurrent: + r.advance() + x.Concurrent = true + } isLocal := r.accept(local) r.expect(infile) x.Path = r.expect(stringLit).lit @@ -397,6 +404,9 @@ func (r *rdParser) parseLoadDataStmt() ast.StmtNode { r.expect(into) r.expect(tableKwd) x.Table = r.parseTableName() + if r.tok() == partition && r.la(1) == int('(') { + x.Partitions = r.parsePartitionNameListOpt() + } if (r.tok() == character || r.tok() == charType) && r.la(1) == set { r.advance() r.advance() @@ -406,10 +416,14 @@ func (r *rdParser) parseLoadDataStmt() ast.StmtNode { x.FieldsInfo = r.parseFieldsClause() x.LinesInfo = r.parseLinesClause() if r.tok() == ignore { - // IgnoreLines: "IGNORE" NUM "LINES" — always shifted here. + // "IGNORE" NUM ("LINES" | "ROWS") — always shifted here; + // Restore() canonicalizes the ROWS spelling to LINES. r.advance() v := getUint64FromNUM(r.expect(intLit).item) - r.expect(lines) + if r.tok() != lines && r.tok() != rows { + r.syntaxError() + } + r.advance() x.IgnoreLines = &v } x.ColumnsAndUserVars = r.parseColumnNameOrUserVarListOptWithBrackets() diff --git a/parser/parse_select.go b/parser/parse_select.go index 8fdb3a4..7fca57b 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -363,8 +363,21 @@ func (r *rdParser) parseSelectTail(st *ast.SelectStmt) { if r.tok() == limit || r.tok() == fetch { st.Limit = r.parseSelectStmtLimit() } + // MySQL also accepts the SelectStmtIntoOption before the locking + // clauses. + if st.SelectIntoOpt == nil && r.tok() == into { + st.SelectIntoOpt = r.parseSelectStmtIntoOption() + } if lock := r.parseSelectLockOpt(); lock != nil { st.LockInfo = lock + // A statement may carry several locking clauses. + for { + more := r.parseSelectLockOpt() + if more == nil { + break + } + st.MoreLockInfos = append(st.MoreLockInfos, more) + } } if st.SelectIntoOpt == nil { if opt := r.parseSelectStmtIntoOption(); opt != nil { @@ -704,6 +717,11 @@ func (r *rdParser) parseSelectStmtIntoOption() *ast.SelectIntoOption { case outfile: r.advance() x := &ast.SelectIntoOption{Tp: ast.SelectIntoOutfile, FileName: r.expect(stringLit).lit} + if (r.tok() == character || r.tok() == charType) && r.la(1) == set { + r.advance() + r.advance() + x.Charset = r.parseCharsetName() + } if fields := r.parseFieldsClause(); fields != nil { x.FieldsInfo = fields } @@ -1061,7 +1079,12 @@ func (r *rdParser) parseTableFactor() ast.ResultSetNode { var ts ast.ResultSetNode if r.try(func() { sub := r.parseSubSelect() - ts = &ast.TableSource{Source: sub.Query.(ast.ResultSetNode), AsName: r.parseTableAsNameOpt()} + src := &ast.TableSource{Source: sub.Query.(ast.ResultSetNode), AsName: r.parseTableAsNameOpt()} + if src.AsName.O != "" && r.tok() == int('(') { + // The optional derived-table column alias list. + src.ColumnNames = r.parseIdentListWithParenOpt() + } + ts = src }) { return ts } diff --git a/parser/testdata/parser/mysql_unsupported_dml/output.sql b/parser/testdata/parser/mysql_unsupported_dml/output.sql index 5d1ef88..c082bdc 100644 --- a/parser/testdata/parser/mysql_unsupported_dml/output.sql +++ b/parser/testdata/parser/mysql_unsupported_dml/output.sql @@ -1,15 +1,15 @@ --- error: line 1 column 40 near "CHARACTER SET utf8mb4 FROM t" +SELECT `a` FROM `t` INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 -- case --- error: line 1 column 47 near "CHARACTER SET utf8mb4 FIELDS TERMINATED BY ','" +SELECT `a` FROM `t` INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' -- case --- error: line 1 column 47 near "FOR UPDATE OF t2 SKIP LOCKED" +SELECT * FROM (`t1`) JOIN `t2` FOR SHARE OF `t1` NOWAIT FOR UPDATE OF `t2` SKIP LOCKED -- case --- error: line 1 column 27 near "FOR UPDATE" +SELECT * FROM `t` FOR UPDATE INTO @`a` -- case --- error: line 1 column 50 near "(c1, c2)" +SELECT * FROM (VALUES ROW(1,2), ROW(3,4)) AS `v`(`c1`, `c2`) -- case --- error: line 1 column 20 near "CONCURRENT INFILE '/tmp/f' IGNORE INTO TABLE t" +LOAD DATA CONCURRENT INFILE '/tmp/f' IGNORE INTO TABLE `t` -- case --- error: line 1 column 48 near "PARTITION (p0) CHARACTER SET utf8mb4" +LOAD DATA INFILE '/tmp/f' INTO TABLE `t` PARTITION (`p0`) CHARACTER SET utf8mb4 -- case --- error: line 1 column 78 near "ROWS" +LOAD DATA INFILE '/tmp/f' INTO TABLE `t` FIELDS TERMINATED BY ',' IGNORE 2 LINES From d309987d2517e3295bf543eb876f65a942640e0a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:35:32 +0000 Subject: [PATCH 10/13] Support JSON_TABLE, JSON_VALUE clauses, CAST AT TIME ZONE and NCHAR, and SOUNDS LIKE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_functions coverage group (MySQL 26.7 §14.17.6, §14.17.3, §14.10): its error goldens turn into Restore() goldens. - JSON_TABLE(expr, path COLUMNS (...)) parses as a table factor: new JSONTableExpr ResultSetNode (ast/mysql_json.go) with JSONTableColumn covering FOR ORDINALITY, typed PATH columns with ON EMPTY/ON ERROR responses, EXISTS PATH, and NESTED PATH sub-columns. The shared JSONOnResponse node models NULL/ERROR/DEFAULT v ON EMPTY|ERROR. - JSON_VALUE with RETURNING and/or ON EMPTY/ON ERROR parses as a new JSONValueExpr; the plain two-argument call stays a generic FuncCallExpr, selected by speculation. - CAST accepts AT TIME ZONE 'tz' between the expression and AS (new FuncCastExpr.AtTimeZone field) and NCHAR[(n)] as a cast type (canonicalized to CHAR in the national character set). - expr SOUNDS LIKE expr parses, desugaring to SOUNDEX(l) = SOUNDEX(r) as MySQL defines the operator. Keyword tables: NESTED, ORDINALITY, PATH, RETURNING, SOUNDS, and ZONE become unreserved keywords, and EMPTY (whose token existed unused) joins the tables; TestKeywordsLength counts updated. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/functions.go | 7 + ast/mysql_json.go | 389 ++++++++++++++++++ parser/keyword_classes.go | 7 + parser/keywords.go | 7 + parser/keywords_test.go | 4 +- parser/misc.go | 7 + parser/parse_expr.go | 15 + parser/parse_func.go | 139 ++++++- parser/parse_select.go | 4 + .../mysql_unsupported_functions/output.sql | 22 +- parser/token_kinds.go | 6 + 11 files changed, 593 insertions(+), 14 deletions(-) create mode 100644 ast/mysql_json.go diff --git a/ast/functions.go b/ast/functions.go index b5d1696..a4ca550 100644 --- a/ast/functions.go +++ b/ast/functions.go @@ -676,6 +676,9 @@ type FuncCastExpr struct { FunctionType CastFunctionType // ExplicitCharSet is true when charset is explicit indicated. ExplicitCharSet bool + // AtTimeZone is the CAST(expr AT TIME ZONE tz AS ...) time zone + // string; empty when absent. + AtTimeZone string } // Restore implements Node interface. @@ -687,6 +690,10 @@ func (n *FuncCastExpr) Restore(ctx *format.RestoreCtx) error { if err := n.Expr.Restore(ctx); err != nil { return annotatef(err, "An error occurred while restore FuncCastExpr.Expr") } + if n.AtTimeZone != "" { + ctx.WriteKeyWord(" AT TIME ZONE ") + ctx.WriteString(n.AtTimeZone) + } ctx.WriteKeyWord(" AS ") n.Tp.RestoreAsCastType(ctx, n.ExplicitCharSet) ctx.WritePlain(")") diff --git a/ast/mysql_json.go b/ast/mysql_json.go new file mode 100644 index 0000000..025ced7 --- /dev/null +++ b/ast/mysql_json.go @@ -0,0 +1,389 @@ +// 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 + +// The MySQL JSON table function and the JSON_VALUE extraction clauses +// (MySQL 26.7 §14.17.6, §14.17.3): JSON_TABLE(expr, path COLUMNS (...)) +// as a table factor, and JSON_VALUE(doc, path RETURNING type ON +// EMPTY/ON ERROR). + +import ( + "io" + "strings" + + "github.com/sqlc-dev/marino/format" + "github.com/sqlc-dev/marino/types" +) + +var ( + _ Node = &JSONOnResponse{} + _ Node = &JSONTableColumn{} + _ ResultSetNode = &JSONTableExpr{} + _ ExprNode = &JSONValueExpr{} +) + +// JSONOnResponseType is what a JSON function produces for an ON EMPTY or +// ON ERROR condition. +type JSONOnResponseType int + +// JSON on-response alternatives. +const ( + // JSONOnResponseNull produces SQL NULL (NULL ON EMPTY/ERROR). + JSONOnResponseNull JSONOnResponseType = iota + // JSONOnResponseError raises an error (ERROR ON EMPTY/ERROR). + JSONOnResponseError + // JSONOnResponseDefault produces the given value (DEFAULT v ON ...). + JSONOnResponseDefault +) + +// JSONOnResponse is one {NULL | ERROR | DEFAULT value} ON {EMPTY|ERROR} +// clause of JSON_VALUE or a JSON_TABLE column. Whether it answers ON +// EMPTY or ON ERROR is determined by the field holding it. +type JSONOnResponse struct { + node + + Tp JSONOnResponseType + // Value is the DEFAULT value; nil for NULL/ERROR. + Value ExprNode +} + +// restoreWithCondition writes the clause followed by " ON ". +func (n *JSONOnResponse) restoreWithCondition(ctx *format.RestoreCtx, cond string) error { + switch n.Tp { + case JSONOnResponseNull: + ctx.WriteKeyWord("NULL") + case JSONOnResponseError: + ctx.WriteKeyWord("ERROR") + case JSONOnResponseDefault: + ctx.WriteKeyWord("DEFAULT ") + if err := n.Value.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONOnResponse.Value") + } + } + ctx.WriteKeyWord(" ON ") + ctx.WriteKeyWord(cond) + return nil +} + +// Restore implements Node interface. The ON condition spelling is owned +// by the enclosing node, which restores through restoreWithCondition; +// Restore alone writes the response value only. +func (n *JSONOnResponse) Restore(ctx *format.RestoreCtx) error { + switch n.Tp { + case JSONOnResponseNull: + ctx.WriteKeyWord("NULL") + case JSONOnResponseError: + ctx.WriteKeyWord("ERROR") + case JSONOnResponseDefault: + ctx.WriteKeyWord("DEFAULT ") + if err := n.Value.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONOnResponse.Value") + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *JSONOnResponse) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*JSONOnResponse) + if n.Value != nil { + node, ok := n.Value.Accept(v) + if !ok { + return n, false + } + n.Value = node.(ExprNode) + } + return v.Leave(n) +} + +// JSONTableColumn is one column specification of JSON_TABLE's COLUMNS +// clause: +// +// name FOR ORDINALITY +// | name type [EXISTS] PATH 'path' [on_empty] [on_error] +// | NESTED [PATH] 'path' COLUMNS (column[, column]...) +type JSONTableColumn struct { + node + + // ForOrdinality selects the counter form; only Name is set. + ForOrdinality bool + Name CIStr + Tp *types.FieldType + // Exists selects the EXISTS PATH form. + Exists bool + // Path is the column's JSON path (a string literal expression); nil + // for the NESTED form, which uses NestedPath. + Path ExprNode + OnEmpty *JSONOnResponse + OnError *JSONOnResponse + + // Nested selects the NESTED PATH form: NestedPath and NestedColumns + // are set, everything else is zero. + Nested bool + NestedPath ExprNode + NestedColumns []*JSONTableColumn +} + +// Restore implements Node interface. +func (n *JSONTableColumn) Restore(ctx *format.RestoreCtx) error { + if n.Nested { + ctx.WriteKeyWord("NESTED PATH ") + if err := n.NestedPath.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONTableColumn.NestedPath") + } + ctx.WriteKeyWord(" COLUMNS ") + ctx.WritePlain("(") + for i, col := range n.NestedColumns { + if i != 0 { + ctx.WritePlain(", ") + } + if err := col.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore JSONTableColumn.NestedColumns[%d]", i) + } + } + ctx.WritePlain(")") + return nil + } + ctx.WriteName(n.Name.O) + if n.ForOrdinality { + ctx.WriteKeyWord(" FOR ORDINALITY") + return nil + } + ctx.WritePlain(" ") + if err := n.Tp.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONTableColumn.Tp") + } + if n.Exists { + ctx.WriteKeyWord(" EXISTS") + } + ctx.WriteKeyWord(" PATH ") + if err := n.Path.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONTableColumn.Path") + } + if n.OnEmpty != nil { + ctx.WritePlain(" ") + if err := n.OnEmpty.restoreWithCondition(ctx, "EMPTY"); err != nil { + return err + } + } + if n.OnError != nil { + ctx.WritePlain(" ") + if err := n.OnError.restoreWithCondition(ctx, "ERROR"); err != nil { + return err + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *JSONTableColumn) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*JSONTableColumn) + if n.Path != nil { + node, ok := n.Path.Accept(v) + if !ok { + return n, false + } + n.Path = node.(ExprNode) + } + if n.OnEmpty != nil { + node, ok := n.OnEmpty.Accept(v) + if !ok { + return n, false + } + n.OnEmpty = node.(*JSONOnResponse) + } + if n.OnError != nil { + node, ok := n.OnError.Accept(v) + if !ok { + return n, false + } + n.OnError = node.(*JSONOnResponse) + } + if n.NestedPath != nil { + node, ok := n.NestedPath.Accept(v) + if !ok { + return n, false + } + n.NestedPath = node.(ExprNode) + } + for i, col := range n.NestedColumns { + node, ok := col.Accept(v) + if !ok { + return n, false + } + n.NestedColumns[i] = node.(*JSONTableColumn) + } + return v.Leave(n) +} + +// JSONTableExpr is the JSON_TABLE table function, used as a table +// factor: JSON_TABLE(expr, path COLUMNS (column[, column]...)). +type JSONTableExpr struct { + node + + Doc ExprNode + Path ExprNode + Columns []*JSONTableColumn +} + +func (*JSONTableExpr) resultSet() {} + +// Restore implements Node interface. +func (n *JSONTableExpr) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("JSON_TABLE") + ctx.WritePlain("(") + if err := n.Doc.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONTableExpr.Doc") + } + ctx.WritePlain(", ") + if err := n.Path.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONTableExpr.Path") + } + ctx.WriteKeyWord(" COLUMNS ") + ctx.WritePlain("(") + for i, col := range n.Columns { + if i != 0 { + ctx.WritePlain(", ") + } + if err := col.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore JSONTableExpr.Columns[%d]", i) + } + } + ctx.WritePlain(")") + ctx.WritePlain(")") + return nil +} + +// Accept implements Node Accept interface. +func (n *JSONTableExpr) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*JSONTableExpr) + node, ok := n.Doc.Accept(v) + if !ok { + return n, false + } + n.Doc = node.(ExprNode) + node, ok = n.Path.Accept(v) + if !ok { + return n, false + } + n.Path = node.(ExprNode) + for i, col := range n.Columns { + colNode, ok := col.Accept(v) + if !ok { + return n, false + } + n.Columns[i] = colNode.(*JSONTableColumn) + } + return v.Leave(n) +} + +// JSONValueExpr is JSON_VALUE(doc, path [RETURNING type] [on_empty] +// [on_error]). The plain two-argument call without any of the optional +// clauses parses as an ordinary FuncCallExpr, not this node. +type JSONValueExpr struct { + funcNode + + Doc ExprNode + Path ExprNode + // Returning is the RETURNING cast target; nil when absent. + Returning *types.FieldType + OnEmpty *JSONOnResponse + OnError *JSONOnResponse +} + +// Restore implements Node interface. +func (n *JSONValueExpr) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("JSON_VALUE") + ctx.WritePlain("(") + if err := n.Doc.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONValueExpr.Doc") + } + ctx.WritePlain(", ") + if err := n.Path.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore JSONValueExpr.Path") + } + if n.Returning != nil { + ctx.WriteKeyWord(" RETURNING ") + n.Returning.RestoreAsCastType(ctx, false) + } + if n.OnEmpty != nil { + ctx.WritePlain(" ") + if err := n.OnEmpty.restoreWithCondition(ctx, "EMPTY"); err != nil { + return err + } + } + if n.OnError != nil { + ctx.WritePlain(" ") + if err := n.OnError.restoreWithCondition(ctx, "ERROR"); err != nil { + return err + } + } + ctx.WritePlain(")") + return nil +} + +// Format the ExprNode into a Writer. +func (n *JSONValueExpr) Format(w io.Writer) { + var sb strings.Builder + if err := n.Restore(format.NewRestoreCtx(format.DefaultRestoreFlags, &sb)); err != nil { + return + } + io.WriteString(w, sb.String()) +} + +// Accept implements Node Accept interface. +func (n *JSONValueExpr) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*JSONValueExpr) + node, ok := n.Doc.Accept(v) + if !ok { + return n, false + } + n.Doc = node.(ExprNode) + node, ok = n.Path.Accept(v) + if !ok { + return n, false + } + n.Path = node.(ExprNode) + if n.OnEmpty != nil { + onNode, ok := n.OnEmpty.Accept(v) + if !ok { + return n, false + } + n.OnEmpty = onNode.(*JSONOnResponse) + } + if n.OnError != nil { + onNode, ok := n.OnError.Accept(v) + if !ok { + return n, false + } + n.OnError = onNode.(*JSONOnResponse) + } + return v.Leave(n) +} diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 4368acc..85fac08 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -403,6 +403,13 @@ var unReservedKeywordNames = []string{ "PERSIST_ONLY", "RELAY", "USER_RESOURCES", + "EMPTY", + "NESTED", + "ORDINALITY", + "PATH", + "RETURNING", + "SOUNDS", + "ZONE", "TPCC", "OLTP_READ_WRITE", "OLTP_READ_ONLY", diff --git a/parser/keywords.go b/parser/keywords.go index a65db78..4c7593f 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -390,6 +390,7 @@ var Keywords = []KeywordsType{ {"DUMPFILE", false, "unreserved"}, {"DUPLICATE", false, "unreserved"}, {"DYNAMIC", false, "unreserved"}, + {"EMPTY", false, "unreserved"}, {"ENABLE", false, "unreserved"}, {"ENABLED", false, "unreserved"}, {"ENCRYPTION", false, "unreserved"}, @@ -514,6 +515,7 @@ var Keywords = []KeywordsType{ {"NAMES", false, "unreserved"}, {"NATIONAL", false, "unreserved"}, {"NCHAR", false, "unreserved"}, + {"NESTED", false, "unreserved"}, {"NEVER", false, "unreserved"}, {"NEXT", false, "unreserved"}, {"NEXTVAL", false, "unreserved"}, @@ -541,6 +543,7 @@ var Keywords = []KeywordsType{ {"OPTIMIZER_COSTS", false, "unreserved"}, {"OPTIONAL", false, "unreserved"}, {"OPTIONS", false, "unreserved"}, + {"ORDINALITY", false, "unreserved"}, {"PACK_KEYS", false, "unreserved"}, {"PAGE", false, "unreserved"}, {"PAGE_CHECKSUM", false, "unreserved"}, @@ -554,6 +557,7 @@ var Keywords = []KeywordsType{ {"PARTITIONS", false, "unreserved"}, {"PASSWORD", false, "unreserved"}, {"PASSWORD_LOCK_TIME", false, "unreserved"}, + {"PATH", false, "unreserved"}, {"PAUSE", false, "unreserved"}, {"PERCENT", false, "unreserved"}, {"PERSIST", false, "unreserved"}, @@ -606,6 +610,7 @@ var Keywords = []KeywordsType{ {"RESTORE", false, "unreserved"}, {"RESTORES", false, "unreserved"}, {"RESUME", false, "unreserved"}, + {"RETURNING", false, "unreserved"}, {"RETURNS", false, "unreserved"}, {"REUSE", false, "unreserved"}, {"REVERSE", false, "unreserved"}, @@ -648,6 +653,7 @@ var Keywords = []KeywordsType{ {"SNAPSHOT", false, "unreserved"}, {"SOME", false, "unreserved"}, {"SONAME", false, "unreserved"}, + {"SOUNDS", false, "unreserved"}, {"SOURCE", false, "unreserved"}, {"SQL_BUFFER_RESULT", false, "unreserved"}, {"SQL_CACHE", false, "unreserved"}, @@ -745,6 +751,7 @@ var Keywords = []KeywordsType{ {"XID", false, "unreserved"}, {"XML", false, "unreserved"}, {"YEAR", false, "unreserved"}, + {"ZONE", false, "unreserved"}, {"ADMIN", false, "tidb"}, {"BATCH", false, "tidb"}, {"BUCKETS", false, "tidb"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index a8f76ac..6c298b8 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(768, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 768) + if !reflect.DeepEqual(775, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 775) } reservedNr := 0 diff --git a/parser/misc.go b/parser/misc.go index 435bd76..d8c9803 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -383,6 +383,7 @@ var tokenMap = map[string]int{ "EACH": each, "ELSE": elseKwd, "ELSEIF": elseIfKwd, + "EMPTY": empty, "ENABLE": enable, "ENABLED": enabled, "ENCLOSED": enclosed, @@ -624,6 +625,7 @@ var tokenMap = map[string]int{ "NATIONAL": national, "NATURAL": natural, "NCHAR": ncharType, + "NESTED": nested, "NEVER": never, "NEXT_ROW_ID": next_row_id, "NEXT": next, @@ -659,6 +661,7 @@ var tokenMap = map[string]int{ "ONLINE": online, "ONLY": only, "OPEN": open, + "ORDINALITY": ordinality, "OPT_RULE_BLACKLIST": optRuleBlacklist, "OPTIMISTIC": optimistic, "OPTIMIZE": optimize, @@ -685,6 +688,7 @@ var tokenMap = map[string]int{ "PARTITIONING": partitioning, "PARTITIONS": partitions, "PASSWORD": password, + "PATH": path, "PAUSE": pause, "PERCENT": percent, "PER_DB": per_db, @@ -792,6 +796,7 @@ var tokenMap = map[string]int{ "HYPO": hypo, "RESUME": resume, "RETURN": returnKwd, + "RETURNING": returning, "RETURNS": returns, "RUN": run, "RUNNING": running, @@ -839,6 +844,7 @@ var tokenMap = map[string]int{ "SNAPSHOT": snapshot, "SOME": some, "SONAME": soname, + "SOUNDS": sounds, "SOURCE": source, "SPATIAL": spatial, "SPEED": speed, @@ -1042,6 +1048,7 @@ var tokenMap = map[string]int{ "XOR": xor, "YEAR_MONTH": yearMonth, "YEAR": yearType, + "ZONE": zone, "ZEROFILL": zerofill, "WAIT": wait, "FAILED_LOGIN_ATTEMPTS": failedLoginAttempts, diff --git a/parser/parse_expr.go b/parser/parse_expr.go index 38f5ec7..9f446e5 100644 --- a/parser/parse_expr.go +++ b/parser/parse_expr.go @@ -336,6 +336,21 @@ func (r *rdParser) parsePredicate() ast.ExprNode { r.advance() pattern := r.parseSimpleExpr() return r.setOrigin(&ast.PatternRegexpExpr{Expr: v, Pattern: pattern, Not: notFlag}, start) + case sounds: + // PredicateExpr: BitExpr "SOUNDS" "LIKE" BitExpr — there is no + // NotSym form. Desugars to SOUNDEX(l) = SOUNDEX(r), which is how + // MySQL defines the operator. + if notFlag { + r.syntaxError() + } + r.advance() + r.expect(like) + rhs := r.parseBitExpr(0) + return r.setOrigin(&ast.BinaryOperationExpr{ + Op: opcode.EQ, + L: &ast.FuncCallExpr{FnName: ast.NewCIStr("soundex"), Args: []ast.ExprNode{v}}, + R: &ast.FuncCallExpr{FnName: ast.NewCIStr("soundex"), Args: []ast.ExprNode{rhs}}, + }, start) case memberof: // PredicateExpr: BitExpr memberof '(' SimpleExpr ')' — there is // no NotSym form, so a preceding NOT makes this token the error. diff --git a/parser/parse_func.go b/parser/parse_func.go index 97b4a8e..9c97ce5 100644 --- a/parser/parse_func.go +++ b/parser/parse_func.go @@ -49,6 +49,15 @@ func (r *rdParser) parseSimpleExprAtom() ast.ExprNode { // The unqualified form requires a raw identifier token. if r.la(1) == int('(') { name := r.cur().lit + if strings.EqualFold(name, "JSON_VALUE") { + // JSON_VALUE with a RETURNING or ON EMPTY/ON ERROR + // clause parses as a JSONValueExpr; the plain call falls + // through to the generic form. + var jv ast.ExprNode + if r.try(func() { jv = r.parseJSONValueExpr() }) { + return r.setOrigin(jv, start) + } + } r.advance() r.advance() args := r.parseExpressionListOpt() @@ -165,10 +174,18 @@ func (r *rdParser) parseSimpleExprAtom() ast.ExprNode { return r.setOrigin(x, start) case builtinCast: - // SimpleExpr: builtinCast '(' Expression "AS" CastType ArrayKwdOpt ')' + // SimpleExpr: builtinCast '(' Expression ["AT" "TIME" "ZONE" + // stringLit] "AS" CastType ArrayKwdOpt ')' r.advance() r.expect(int('(')) expr := r.parseExpression() + var atTimeZone string + if r.tok() == at { + r.advance() + r.expect(timeType) + r.expect(zone) + atTimeZone = r.expect(stringLit).lit + } r.expect(as) tp := r.parseCastType() isArray := r.accept(array) @@ -192,6 +209,7 @@ func (r *rdParser) parseSimpleExprAtom() ast.ExprNode { Tp: tp, FunctionType: ast.CastFunction, ExplicitCharSet: explicitCharset, + AtTimeZone: atTimeZone, }, start) case jsonSumCrc32: // SimpleExpr: jsonSumCrc32 '(' Expression "AS" CastType "ARRAY" ')' @@ -1285,6 +1303,16 @@ func (r *rdParser) parseCastType() *types.FieldType { tp.SetCollate(charset.CollationBin) tp.AddFlag(mysql.BinaryFlag) return tp + case ncharType: + // "NCHAR" OptFieldLen — the national char cast, which is CHAR in + // the national character set (the connection charset here). + r.advance() + flen := r.parseOptFieldLen() + tp := types.NewFieldType(mysql.TypeVarString) + tp.SetFlen(flen) + tp.SetCharset(r.p.charset) + tp.SetCollate(r.p.collation) + return tp case charType, character: // Char OptFieldLen OptBinary r.advance() @@ -1555,3 +1583,112 @@ func (r *rdParser) endOffsetAt(offset int) int { } return offset } + +// parseJSONValueExpr implements the extended JSON_VALUE form +// (MySQL 26.7 §14.17.3): +// +// "JSON_VALUE" '(' Expression ',' Expression ["RETURNING" CastType] +// [JSONOnResponse "ON" "EMPTY"] [JSONOnResponse "ON" "ERROR"] ')' +// +// At least one of the optional clauses must be present; the plain +// two-argument call stays a generic FuncCallExpr (the caller speculates, +// so failing here selects that route). +func (r *rdParser) parseJSONValueExpr() ast.ExprNode { + r.expect(identifier) // JSON_VALUE + r.expect(int('(')) + x := &ast.JSONValueExpr{Doc: r.parseExpression()} + r.expect(int(',')) + x.Path = r.parseExpression() + if r.accept(returning) { + x.Returning = r.parseCastType() + } + r.parseJSONOnResponses(&x.OnEmpty, &x.OnError) + if x.Returning == nil && x.OnEmpty == nil && x.OnError == nil { + r.syntaxError() + } + r.expect(int(')')) + return x +} + +// parseJSONOnResponses parses the [on_empty] [on_error] clauses shared by +// JSON_VALUE and JSON_TABLE columns: +// ("NULL" | "ERROR" | "DEFAULT" SimpleExpr) "ON" ("EMPTY" | "ERROR"). +func (r *rdParser) parseJSONOnResponses(onEmpty, onError **ast.JSONOnResponse) { + for r.tok() == null || r.tok() == errorKwd || r.tok() == defaultKwd { + resp := &ast.JSONOnResponse{} + switch r.tok() { + case null: + r.advance() + resp.Tp = ast.JSONOnResponseNull + case errorKwd: + r.advance() + resp.Tp = ast.JSONOnResponseError + case defaultKwd: + r.advance() + resp.Tp = ast.JSONOnResponseDefault + resp.Value = r.parseSimpleExpr() + } + r.expect(on) + if r.accept(empty) { + *onEmpty = resp + } else { + r.expect(errorKwd) + *onError = resp + } + } +} + +// parseJSONTableExpr implements the JSON_TABLE table function +// (MySQL 26.7 §14.17.6): +// +// "JSON_TABLE" '(' Expression ',' Expression "COLUMNS" +// '(' JSONTableColumn (',' JSONTableColumn)* ')' ')' +func (r *rdParser) parseJSONTableExpr() *ast.JSONTableExpr { + r.expect(identifier) // JSON_TABLE + r.expect(int('(')) + x := &ast.JSONTableExpr{Doc: r.parseExpression()} + r.expect(int(',')) + x.Path = r.parseExpression() + r.expect(columns) + r.expect(int('(')) + x.Columns = []*ast.JSONTableColumn{r.parseJSONTableColumn()} + for r.accept(int(',')) { + x.Columns = append(x.Columns, r.parseJSONTableColumn()) + } + r.expect(int(')')) + r.expect(int(')')) + return x +} + +// parseJSONTableColumn implements one COLUMNS entry of JSON_TABLE: +// +// Identifier "FOR" "ORDINALITY" +// | Identifier Type ["EXISTS"] "PATH" Expression [on_empty] [on_error] +// | "NESTED" ["PATH"] Expression "COLUMNS" '(' ... ')' +func (r *rdParser) parseJSONTableColumn() *ast.JSONTableColumn { + if r.tok() == nested { + r.advance() + r.accept(path) + col := &ast.JSONTableColumn{Nested: true, NestedPath: r.parseExpression()} + r.expect(columns) + r.expect(int('(')) + col.NestedColumns = []*ast.JSONTableColumn{r.parseJSONTableColumn()} + for r.accept(int(',')) { + col.NestedColumns = append(col.NestedColumns, r.parseJSONTableColumn()) + } + r.expect(int(')')) + return col + } + name := ast.NewCIStr(r.parseIdentifier()) + if r.tok() == forKwd { + r.advance() + r.expect(ordinality) + return &ast.JSONTableColumn{Name: name, ForOrdinality: true} + } + col := &ast.JSONTableColumn{Name: name, Tp: r.parseType()} + col.Exists = r.accept(exists) + r.expect(path) + col.Path = r.parseExpression() + r.parseJSONOnResponses(&col.OnEmpty, &col.OnError) + return col +} diff --git a/parser/parse_select.go b/parser/parse_select.go index 7fca57b..aaa00c3 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -1103,6 +1103,10 @@ func (r *rdParser) parseTableFactor() ast.ResultSetNode { ts.Lateral = true ts.ColumnNames = r.parseIdentListWithParenOpt() return ts + case r.tok() == identifier && r.la(1) == int('(') && strings.EqualFold(r.cur().lit, "JSON_TABLE"): + // TableFactor: the JSON_TABLE table function TableAsNameOpt + jt := r.parseJSONTableExpr() + return &ast.TableSource{Source: jt, AsName: r.parseTableAsNameOpt()} default: // TableFactor: TableName PartitionNameListOpt TableAsNameOpt // AsOfClauseOpt IndexHintListOpt TableSampleOpt diff --git a/parser/testdata/parser/mysql_unsupported_functions/output.sql b/parser/testdata/parser/mysql_unsupported_functions/output.sql index b360ed2..a6a3299 100644 --- a/parser/testdata/parser/mysql_unsupported_functions/output.sql +++ b/parser/testdata/parser/mysql_unsupported_functions/output.sql @@ -1,21 +1,21 @@ --- error: line 1 column 24 near "NCHAR(5))" +SELECT CAST(_UTF8MB4'a' AS CHAR(5)) -- case --- error: line 1 column 24 near "NCHAR)" +SELECT CAST(_UTF8MB4'a' AS CHAR) -- case --- error: line 1 column 45 near "AT TIME ZONE 'UTC' AS DATETIME)" +SELECT CAST(TIMESTAMP '2024-01-01 12:00:00' AT TIME ZONE 'UTC' AS DATETIME) -- case --- error: line 1 column 45 near "AT TIME ZONE '+00:00' AS DATETIME(3))" +SELECT CAST(TIMESTAMP '2024-01-01 12:00:00' AT TIME ZONE '+00:00' AS DATETIME(3)) -- case --- error: line 1 column 25 near "('[{"a":1}]', '$[*]' COLUMNS (a INT PATH '$.a')) AS jt" +SELECT * FROM JSON_TABLE(_UTF8MB4'[{"a":1}]', _UTF8MB4'$[*]' COLUMNS (`a` INT PATH _UTF8MB4'$.a')) AS `jt` -- case --- error: line 1 column 25 near "('[]', '$[*]' COLUMNS (ord FOR ORDINALITY, x VARCHAR(10) PATH '$.x' DEFAULT '"d"' ON EMPTY NULL ON ERROR, y INT EXISTS PATH '$.y', NESTED PATH '$.z[*]' COLUMNS (z INT PATH '$'))) AS jt" +SELECT * FROM JSON_TABLE(_UTF8MB4'[]', _UTF8MB4'$[*]' COLUMNS (`ord` FOR ORDINALITY, `x` VARCHAR(10) PATH _UTF8MB4'$.x' DEFAULT _UTF8MB4'"d"' ON EMPTY NULL ON ERROR, `y` INT EXISTS PATH _UTF8MB4'$.y', NESTED PATH _UTF8MB4'$.z[*]' COLUMNS (`z` INT PATH _UTF8MB4'$'))) AS `jt` -- case --- error: line 1 column 37 near "(t.j, '$[*]' COLUMNS (x VARCHAR(10) PATH '$.x' ERROR ON EMPTY)) AS jt" +SELECT `t`.`id`,`jt`.`x` FROM (`t`) JOIN JSON_TABLE(`t`.`j`, _UTF8MB4'$[*]' COLUMNS (`x` VARCHAR(10) PATH _UTF8MB4'$.x' ERROR ON EMPTY)) AS `jt` -- case --- error: line 1 column 45 near "RETURNING UNSIGNED)" +SELECT JSON_VALUE(_UTF8MB4'{"a":42}', _UTF8MB4'$.a' RETURNING UNSIGNED) -- case --- error: line 1 column 47 near "RETURNING DECIMAL(5,2) DEFAULT 0 ON EMPTY DEFAULT 99 ON ERROR)" +SELECT JSON_VALUE(_UTF8MB4'{"p":12.5}', _UTF8MB4'$.d' RETURNING DECIMAL(5, 2) DEFAULT 0 ON EMPTY DEFAULT 99 ON ERROR) -- case --- error: line 1 column 39 near "RETURNING CHAR(8) NULL ON EMPTY ERROR ON ERROR)" +SELECT JSON_VALUE(_UTF8MB4'{}', _UTF8MB4'$.x' RETURNING CHAR(8) NULL ON EMPTY ERROR ON ERROR) -- case --- error: line 1 column 22 near "LIKE 'b'" +SELECT SOUNDEX(_UTF8MB4'a')=SOUNDEX(_UTF8MB4'b') diff --git a/parser/token_kinds.go b/parser/token_kinds.go index 97b3d3a..c9e3560 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -1034,6 +1034,12 @@ const ( userResources = 58334 loop = 58335 condition = 58336 + sounds = 58337 + zone = 58338 + nested = 58340 + ordinality = 58341 + path = 58342 + returning = 58343 wrapper = 58319 write = 57592 x509 = 57993 From 787ae90e9cad6a93a5b4b2ee23feaf3943fab8a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:40:53 +0000 Subject: [PATCH 11/13] Support stored routine characteristics, LANGUAGE JAVASCRIPT bodies, dollar quoting, and the USING clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_routines coverage group (MySQL 26.7 §15.1.17, §11.1.1, §15.1.19): its error goldens turn into Restore() goldens. - CREATE PROCEDURE now accepts the full routine characteristic list (COMMENT, LANGUAGE, [NOT] DETERMINISTIC, CONTAINS/NO SQL, READS/MODIFIES SQL DATA, SQL SECURITY), reusing the RoutineCharacteristics machinery CREATE FUNCTION already had (new ProcedureInfo.Characteristics field). - Dollar-quoted strings ($$...$$ and $tag$...$tag$) lex as string literals; an opening tag without a matching closing tag falls back to the '$'-led identifier reading. Restore() writes them as ordinary quoted strings. - LANGUAGE JAVASCRIPT bodies: CREATE PROCEDURE and CREATE FUNCTION accept AS 'code' (string or dollar-quoted) via the new CodeBody fields, plus the USING (library [AS alias], ...) imports clause (new RoutineImport node and Imports fields). CREATE LIBRARY $$-bodies now parse through the same lexer change. - ProcedureInfo.Accept no longer dereferences a nil body. Restore() prints procedure parameter types via CompactStr, which substitutes default display widths; the round-trip harness canonicalizes parsed parameter types the same way and clears the recorded parameter source text (test-only nodeTextCleaner cases). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/mysql_ddl.go | 49 +++++++++++++++++- ast/procedure.go | 35 ++++++++++--- parser/lexer.go | 23 +++++++++ parser/misc.go | 3 +- parser/parse_mysql_ddl.go | 32 ++++++++++++ parser/parse_procedure.go | 17 +++++-- parser/parser_test.go | 18 +++++++ .../mysql_unsupported_routines/output.sql | 50 +++++++++---------- 8 files changed, 190 insertions(+), 37 deletions(-) diff --git a/ast/mysql_ddl.go b/ast/mysql_ddl.go index 666c229..e1e16a7 100644 --- a/ast/mysql_ddl.go +++ b/ast/mysql_ddl.go @@ -676,7 +676,46 @@ type CreateFunctionStmt struct { Params []*FunctionParam ReturnType *types.FieldType Characteristics RoutineCharacteristics - Body StmtNode + // Imports is the USING (library [AS alias], ...) clause of LANGUAGE + // JAVASCRIPT routines; empty when absent. + Imports []*RoutineImport + // Body is the SQL routine body; nil when the routine uses the AS + // string form (CodeBody). + Body StmtNode + // CodeBody is the AS 'code' body of LANGUAGE JAVASCRIPT routines + // (a string or dollar-quoted literal); nil for SQL bodies. + CodeBody *string +} + +// RoutineImport is one entry of the USING (library [AS alias], ...) +// clause of a LANGUAGE JAVASCRIPT routine. +type RoutineImport struct { + Library *TableName + Alias CIStr // empty when absent; restored with AS +} + +// restoreRoutineImports writes a USING clause with one leading space, or +// nothing for an empty list. +func restoreRoutineImports(ctx *format.RestoreCtx, imports []*RoutineImport, what string) error { + if len(imports) == 0 { + return nil + } + ctx.WriteKeyWord(" USING ") + ctx.WritePlain("(") + for i, imp := range imports { + if i != 0 { + ctx.WritePlain(", ") + } + if err := imp.Library.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore %s.Imports[%d]", what, i) + } + if imp.Alias.O != "" { + ctx.WriteKeyWord(" AS ") + ctx.WriteName(imp.Alias.O) + } + } + ctx.WritePlain(")") + return nil } // Restore implements Node interface. @@ -711,6 +750,14 @@ func (n *CreateFunctionStmt) Restore(ctx *format.RestoreCtx) error { if err := n.Characteristics.Restore(ctx); err != nil { return err } + if err := restoreRoutineImports(ctx, n.Imports, "CreateFunctionStmt"); err != nil { + return err + } + if n.CodeBody != nil { + ctx.WriteKeyWord(" AS ") + ctx.WriteString(*n.CodeBody) + return nil + } ctx.WritePlain(" ") if err := n.Body.Restore(ctx); err != nil { return annotate(err, "An error occurred while restore CreateFunctionStmt.Body") diff --git a/ast/procedure.go b/ast/procedure.go index 8fcdaec..9f51976 100644 --- a/ast/procedure.go +++ b/ast/procedure.go @@ -237,8 +237,17 @@ type ProcedureInfo struct { IfNotExists bool ProcedureName *TableName ProcedureParam []*StoreParameter //procedure param - ProcedureBody StmtNode //procedure body statement + ProcedureBody StmtNode //procedure body statement; nil when the routine uses the AS string form (CodeBody) ProcedureParamStr string //procedure parameter string + // Characteristics are the routine characteristics (COMMENT, + // LANGUAGE, [NOT] DETERMINISTIC, SQL data access, SQL SECURITY). + Characteristics RoutineCharacteristics + // Imports is the USING (library [AS alias], ...) clause of LANGUAGE + // JAVASCRIPT routines; empty when absent. + Imports []*RoutineImport + // CodeBody is the AS 'code' body of LANGUAGE JAVASCRIPT routines + // (a string or dollar-quoted literal); nil for SQL bodies. + CodeBody *string // Definer is the DEFINER = user clause; nil when absent. Definer *auth.UserIdentity } @@ -272,7 +281,19 @@ func (n *ProcedureInfo) Restore(ctx *format.RestoreCtx) error { return err } } - ctx.WritePlain(") ") + ctx.WritePlain(")") + if err := n.Characteristics.Restore(ctx); err != nil { + return err + } + if err := restoreRoutineImports(ctx, n.Imports, "ProcedureInfo"); err != nil { + return err + } + if n.CodeBody != nil { + ctx.WriteKeyWord(" AS ") + ctx.WriteString(*n.CodeBody) + return nil + } + ctx.WritePlain(" ") err = (n.ProcedureBody).Restore(ctx) if err != nil { return err @@ -294,11 +315,13 @@ func (n *ProcedureInfo) Accept(v Visitor) (Node, bool) { } n.ProcedureParam[i] = node.(*StoreParameter) } - node, ok := n.ProcedureBody.Accept(v) - if !ok { - return n, false + if n.ProcedureBody != nil { + node, ok := n.ProcedureBody.Accept(v) + if !ok { + return n, false + } + n.ProcedureBody = node.(StmtNode) } - n.ProcedureBody = node.(StmtNode) return v.Leave(n) } diff --git a/parser/lexer.go b/parser/lexer.go index 84ba60b..6959429 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -662,6 +662,29 @@ func scanIdentifier(s *Scanner) (int, Pos, string) { return identifier, pos, s.r.data(&pos) } +// startWithDollar scans either a dollar-quoted string — $tag$ ... $tag$ +// with tag one or more identifier characters or empty, used by LANGUAGE +// JAVASCRIPT routine bodies (MySQL 26.7 §11.1.1) — or an ordinary +// identifier starting with '$'. An opening tag without a matching +// closing tag falls back to the identifier reading. +func startWithDollar(s *Scanner) (tok int, pos Pos, lit string) { + pos = s.r.pos() + stream := s.r.s[pos.Offset:] + end := 1 + for end < len(stream) && (isLetter(stream[end]) || isDigit(stream[end]) || stream[end] == '_') { + end++ + } + if end < len(stream) && stream[end] == '$' { + tag := stream[:end+1] + rest := stream[end+1:] + if idx := strings.Index(rest, tag); idx >= 0 { + s.r.incN(len(tag)*2 + idx) + return stringLit, pos, rest[:idx] + } + } + return scanIdentifier(s) +} + func scanIdentifierOrString(s *Scanner) (tok int, lit string) { ch1 := s.r.peek() switch ch1 { diff --git a/parser/misc.go b/parser/misc.go index d8c9803..66b6286 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -141,7 +141,8 @@ func init() { initTokenFunc("Nn", startWithNn) initTokenFunc("Bb", startWithBb) initTokenFunc(".", startWithDot) - initTokenFunc("_$ACDEFGHIJKLMOPQRSTUVWYZacdefghijklmopqrstuvwyz", scanIdentifier) + initTokenFunc("_ACDEFGHIJKLMOPQRSTUVWYZacdefghijklmopqrstuvwyz", scanIdentifier) + initTokenFunc("$", startWithDollar) initTokenFunc("`", scanQuotedIdent) initTokenFunc("0123456789", startWithNumber) initTokenFunc("'\"", startString) diff --git a/parser/parse_mysql_ddl.go b/parser/parse_mysql_ddl.go index 3ba5d77..f41a31a 100644 --- a/parser/parse_mysql_ddl.go +++ b/parser/parse_mysql_ddl.go @@ -381,10 +381,42 @@ func (r *rdParser) finishCreateStoredFunctionStmt(definer *auth.UserIdentity, if r.expect(returns) stmt.ReturnType = r.parseType() stmt.Characteristics = r.parseRoutineCharacteristics() + stmt.Imports = r.parseRoutineImportsOpt() + if r.tok() == as { + // The AS 'code' body of a LANGUAGE JAVASCRIPT routine. + r.advance() + s := r.expect(stringLit).lit + stmt.CodeBody = &s + return stmt + } stmt.Body = r.parseRoutineBody() return stmt } +// parseRoutineImportsOpt implements the USING clause of LANGUAGE +// JAVASCRIPT routines: +// empty | "USING" '(' TableName ["AS" Identifier] (',' ...)* ')'. +func (r *rdParser) parseRoutineImportsOpt() []*ast.RoutineImport { + if r.tok() != using { + return nil + } + r.advance() + r.expect(int('(')) + var imports []*ast.RoutineImport + for { + imp := &ast.RoutineImport{Library: r.parseTableName()} + if r.accept(as) { + imp.Alias = ast.NewCIStr(r.parseIdentifier()) + } + imports = append(imports, imp) + if !r.accept(int(',')) { + break + } + } + r.expect(int(')')) + return imports +} + // parseAlterFunctionStmt implements AlterFunctionStmt: // "ALTER" "FUNCTION" TableName RoutineCharacteristics. func (r *rdParser) parseAlterFunctionStmt() ast.StmtNode { diff --git a/parser/parse_procedure.go b/parser/parse_procedure.go index 0fbe0a1..2bf3f34 100644 --- a/parser/parse_procedure.go +++ b/parser/parse_procedure.go @@ -88,16 +88,25 @@ func (r *rdParser) parseCreateProcedureStmt() ast.StmtNode { lparen := r.expect(int('(')) params := r.parseOptSpPdparams() rparen := r.expect(int(')')) - bodyStart := r.cur().offset - body := r.parseProcedureProcStmt() x := &ast.ProcedureInfo{ IfNotExists: ifNotExists, ProcedureName: procName, ProcedureParam: params, - ProcedureBody: body, Definer: definerV, } - r.p.setNodeText(body, strings.TrimSpace(r.src[bodyStart:r.cur().offset])) + x.Characteristics = r.parseRoutineCharacteristics() + x.Imports = r.parseRoutineImportsOpt() + if r.tok() == as { + // The AS 'code' body of a LANGUAGE JAVASCRIPT routine. + r.advance() + s := r.expect(stringLit).lit + x.CodeBody = &s + } else { + bodyStart := r.cur().offset + body := r.parseProcedureProcStmt() + x.ProcedureBody = body + r.p.setNodeText(body, strings.TrimSpace(r.src[bodyStart:r.cur().offset])) + } startOffset := lparen.offset if r.src[startOffset] == '(' { startOffset++ diff --git a/parser/parser_test.go b/parser/parser_test.go index b88b7b1..348b40d 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -29,6 +29,7 @@ import ( . "github.com/sqlc-dev/marino/format" "github.com/sqlc-dev/marino/mysql" "github.com/sqlc-dev/marino/opcode" + "github.com/sqlc-dev/marino/types" "github.com/sqlc-dev/marino/parser" "github.com/sqlc-dev/marino/terror" ) @@ -3872,6 +3873,23 @@ func (checker *nodeTextCleaner) Enter(in ast.Node) (out ast.Node, skipChildren b for _, stmt := range node.ProcedureProcStmts { stmt.Accept(&tmpCleaner) } + case *ast.ProcedureInfo: + // The parameter list's recorded source text is not normalized by + // Restore(). + node.ProcedureParamStr = "" + case *ast.StoreParameter: + // Restore() prints the type via CompactStr, which substitutes + // the default display width for an unspecified one; canonicalize + // the parsed type the same way so IN a INT compares deep-equal + // with its restored IN a INT(11) spelling. + if node.ParamType.GetFlen() == types.UnspecifiedLength { + flen, _ := mysql.GetDefaultFieldLengthAndDecimal(node.ParamType.GetType()) + node.ParamType.SetFlen(flen) + } + if node.ParamType.GetDecimal() == types.UnspecifiedLength { + _, decimal := mysql.GetDefaultFieldLengthAndDecimal(node.ParamType.GetType()) + node.ParamType.SetDecimal(decimal) + } } return in, false } diff --git a/parser/testdata/parser/mysql_unsupported_routines/output.sql b/parser/testdata/parser/mysql_unsupported_routines/output.sql index a994d56..42123c7 100644 --- a/parser/testdata/parser/mysql_unsupported_routines/output.sql +++ b/parser/testdata/parser/mysql_unsupported_routines/output.sql @@ -1,49 +1,49 @@ --- error: line 1 column 29 near "COMMENT 'c' SELECT 1" +CREATE PROCEDURE `p`() COMMENT 'c' SELECT 1 -- case --- error: line 1 column 30 near "LANGUAGE SQL SELECT 1" +CREATE PROCEDURE `p`() LANGUAGE SQL SELECT 1 -- case --- error: line 1 column 25 near "NOT DETERMINISTIC SELECT 1" +CREATE PROCEDURE `p`() NOT DETERMINISTIC SELECT 1 -- case --- error: line 1 column 30 near "CONTAINS SQL SELECT 1" +CREATE PROCEDURE `p`() CONTAINS SQL SELECT 1 -- case --- error: line 1 column 24 near "NO SQL SELECT 1" +CREATE PROCEDURE `p`() NO SQL SELECT 1 -- case --- error: line 1 column 27 near "READS SQL DATA SELECT 1" +CREATE PROCEDURE `p`() READS SQL DATA SELECT 1 -- case --- error: line 1 column 30 near "MODIFIES SQL DATA SELECT 1" +CREATE PROCEDURE `p`() MODIFIES SQL DATA SELECT 1 -- case --- error: line 1 column 25 near "SQL SECURITY DEFINER SELECT 1" +CREATE PROCEDURE `p`() SQL SECURITY DEFINER SELECT 1 -- case --- error: line 1 column 25 near "SQL SECURITY INVOKER SELECT 1" +CREATE PROCEDURE `p`() SQL SECURITY INVOKER SELECT 1 -- case --- error: line 1 column 86 near "COMMENT 'c' LANGUAGE SQL NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER SELECT 1" +CREATE DEFINER = `u`@`h` PROCEDURE `p`( IN `a` INT(11), OUT `b` VARCHAR(10), INOUT `c` BIGINT(20)) COMMENT 'c' LANGUAGE SQL NOT DETERMINISTIC CONTAINS SQL SQL SECURITY DEFINER SELECT 1 -- case --- error: line 1 column 92 near "AS $$ +CREATE FUNCTION `js_add`(`a` INT, `b` INT) RETURNS INT LANGUAGE JAVASCRIPT DETERMINISTIC NO SQL AS ' return a + b; -$$" +' -- case --- error: line 1 column 53 near "LANGUAGE JAVASCRIPT AS $$ +CREATE PROCEDURE `js_log`( IN `msg` VARCHAR(255)) LANGUAGE JAVASCRIPT AS ' console.log(msg); -$$" +' -- case --- error: line 1 column 100 near "AS $body$ +CREATE FUNCTION `js_q`(`s` VARCHAR(64)) RETURNS VARCHAR(128) LANGUAGE JAVASCRIPT DETERMINISTIC NO SQL AS ' return "got: " + s; -$body$" +' -- case --- error: line 1 column 85 near "AS 'return a'" +CREATE FUNCTION `js_str`(`a` INT) RETURNS INT LANGUAGE JAVASCRIPT DETERMINISTIC NO SQL AS 'return a' -- case --- error: line 1 column 49 near "$$ +CREATE LIBRARY `lib_demo` LANGUAGE JAVASCRIPT AS ' export function inc(n) { return n + 1; } -$$" +' -- case --- error: line 1 column 68 near "$$ +CREATE LIBRARY IF NOT EXISTS `test`.`lib_demo` LANGUAGE JAVASCRIPT AS ' export const PI = 3.14159; -$$" +' -- case --- error: line 1 column 88 near "USING (lib_demo) AS $$ +CREATE FUNCTION `js_inc`(`a` INT) RETURNS INT LANGUAGE JAVASCRIPT DETERMINISTIC NO SQL USING (`lib_demo`) AS ' return lib_demo.inc(a); -$$" +' -- case --- error: line 1 column 34 near "LANGUAGE JAVASCRIPT USING (test.lib_demo AS d, other_lib) AS $$ +CREATE PROCEDURE `p_demo`() LANGUAGE JAVASCRIPT USING (`test`.`lib_demo` AS `d`, `other_lib`) AS ' d.inc(1); -$$" +' From 2599adb7da92f1dee025e1a550153fe4d32c8496 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 00:49:26 +0000 Subject: [PATCH 12/13] Support RANDOM PASSWORD, multi-factor auth, dual passwords, DEFAULT ROLE, GRANT AS, and the REVOKE variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the mysql_unsupported_account coverage group (MySQL 26.7 §15.7.1): its error goldens turn into Restore() goldens. - IDENTIFIED [WITH plugin] BY RANDOM PASSWORD parses (new AuthOption.ByRandomPassword), and every auth option accepts the dual-password clauses REPLACE 'current' and RETAIN CURRENT PASSWORD (new ReplacePassword/RetainCurrentPassword fields). ALTER USER ... DISCARD OLD PASSWORD parses as an AuthOption carrying only the new DiscardOldPassword flag. - Multi-factor authentication: user specs chain AND IDENTIFIED ... factors (new UserSpec.MoreAuthOpts), and ALTER USER gains ADD/MODIFY/DROP n FACTOR plus the n FACTOR INITIATE/FINISH REGISTRATION and UNREGISTER steps (new AlterUserFactor clause and AlterUserStmt.Factors field). - CREATE USER and ALTER USER accept DEFAULT ROLE {NONE | ALL | roles} (new UserDefaultRoles clause), and the PASSWORD REQUIRE CURRENT [OPTIONAL] account policies join the existing DEFAULT form (whose Restore case was missing). - GRANT role TO user accepts WITH ADMIN OPTION (new GrantRoleStmt.AdminOption), and GRANT ... accepts AS user [WITH ROLE DEFAULT|NONE|ALL|ALL EXCEPT r...|r...] (new GrantAs clause reusing SetRoleStmtType). - REVOKE gains IF EXISTS and IGNORE UNKNOWN USER on both the privilege and role forms, and REVOKE PROXY ON user FROM users parses as a new RevokeProxyStmt mirroring GrantProxyStmt. - SET PASSWORD gains TO RANDOM, REPLACE 'old', and RETAIN CURRENT PASSWORD (new SetPwdStmt fields). Keyword tables: CHALLENGE_RESPONSE, FACTOR, FINISH, INITIATE, OLD, RANDOM, REGISTRATION, RETAIN, and UNREGISTER become unreserved keywords; TestKeywordsLength counts updated. testdata/errors.json is unaffected. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- ast/misc.go | 301 +++++++++++++++++- ast/sem.go | 7 + parser/keyword_classes.go | 9 + parser/keywords.go | 9 + parser/keywords_test.go | 4 +- parser/misc.go | 9 + parser/parse_alter.go | 73 ++++- parser/parse_create_misc.go | 35 +- parser/parse_grant.go | 176 ++++++++-- parser/parse_set.go | 30 +- .../mysql_unsupported_account/output.sql | 76 ++--- parser/token_kinds.go | 9 + 12 files changed, 657 insertions(+), 81 deletions(-) diff --git a/ast/misc.go b/ast/misc.go index 37e33fd..1885d13 100644 --- a/ast/misc.go +++ b/ast/misc.go @@ -102,22 +102,45 @@ type AuthOption struct { ByHashString bool HashString string AuthPlugin string + // ByRandomPassword is the BY RANDOM PASSWORD form. + ByRandomPassword bool + // ReplacePassword is the REPLACE 'current' dual-password clause; + // nil when absent. + ReplacePassword *string + // RetainCurrentPassword is the RETAIN CURRENT PASSWORD clause. + RetainCurrentPassword bool + // DiscardOldPassword selects the DISCARD OLD PASSWORD form, which + // replaces the whole IDENTIFIED clause; no other field is set. + DiscardOldPassword bool } // Restore implements Node interface. func (n *AuthOption) Restore(ctx *format.RestoreCtx) error { + if n.DiscardOldPassword { + ctx.WriteKeyWord("DISCARD OLD PASSWORD") + return nil + } ctx.WriteKeyWord("IDENTIFIED") if n.AuthPlugin != "" { ctx.WriteKeyWord(" WITH ") ctx.WriteString(n.AuthPlugin) } - if n.ByAuthString { + if n.ByRandomPassword { + ctx.WriteKeyWord(" BY RANDOM PASSWORD") + } else if n.ByAuthString { ctx.WriteKeyWord(" BY ") ctx.WriteString(n.AuthString) } else if n.ByHashString { ctx.WriteKeyWord(" AS ") ctx.WriteString(n.HashString) } + if n.ReplacePassword != nil { + ctx.WriteKeyWord(" REPLACE ") + ctx.WriteString(*n.ReplacePassword) + } + if n.RetainCurrentPassword { + ctx.WriteKeyWord(" RETAIN CURRENT PASSWORD") + } return nil } @@ -1594,6 +1617,14 @@ type SetPwdStmt struct { User *auth.UserIdentity Password string + // ToRandom is the SET PASSWORD ... TO RANDOM form; Password is + // empty then. + ToRandom bool + // ReplacePassword is the REPLACE 'current' dual-password clause; + // nil when absent. + ReplacePassword *string + // RetainCurrentPassword is the RETAIN CURRENT PASSWORD clause. + RetainCurrentPassword bool } // Restore implements Node interface. @@ -1605,8 +1636,19 @@ func (n *SetPwdStmt) Restore(ctx *format.RestoreCtx) error { return annotate(err, "An error occurred while restore SetPwdStmt.User") } } - ctx.WritePlain("=") - ctx.WriteString(n.Password) + if n.ToRandom { + ctx.WriteKeyWord(" TO RANDOM") + } else { + ctx.WritePlain("=") + ctx.WriteString(n.Password) + } + if n.ReplacePassword != nil { + ctx.WriteKeyWord(" REPLACE ") + ctx.WriteString(*n.ReplacePassword) + } + if n.RetainCurrentPassword { + ctx.WriteKeyWord(" RETAIN CURRENT PASSWORD") + } return nil } @@ -1735,6 +1777,9 @@ type UserSpec struct { User *auth.UserIdentity AuthOpt *AuthOption IsRole bool + // MoreAuthOpts are the second and third authentication factors + // (AND IDENTIFIED ...); empty when absent. + MoreAuthOpts []*AuthOption } // Restore implements Node interface. @@ -1748,6 +1793,12 @@ func (n *UserSpec) Restore(ctx *format.RestoreCtx) error { return annotate(err, "An error occurred while restore UserSpec.AuthOpt") } } + for i, opt := range n.MoreAuthOpts { + ctx.WriteKeyWord(" AND ") + if err := opt.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore UserSpec.MoreAuthOpts[%d]", i) + } + } return nil } @@ -1883,6 +1934,9 @@ const ( PasswordRequireCurrentDefault UserResourceGroupName + + PasswordRequireCurrent + PasswordRequireCurrentOptional ) type PasswordOrLockOption struct { @@ -1925,6 +1979,12 @@ func (p *PasswordOrLockOption) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord(" DAY") case PasswordReuseDefault: ctx.WriteKeyWord("PASSWORD REUSE INTERVAL DEFAULT") + case PasswordRequireCurrent: + ctx.WriteKeyWord("PASSWORD REQUIRE CURRENT") + case PasswordRequireCurrentDefault: + ctx.WriteKeyWord("PASSWORD REQUIRE CURRENT DEFAULT") + case PasswordRequireCurrentOptional: + ctx.WriteKeyWord("PASSWORD REQUIRE CURRENT OPTIONAL") default: return fmt.Errorf("Unsupported PasswordOrLockOption.Type %d", p.Type) } @@ -1957,6 +2017,98 @@ func (c *ResourceGroupNameOption) Restore(ctx *format.RestoreCtx) error { return nil } +// UserDefaultRoles is the DEFAULT ROLE clause of CREATE USER and ALTER +// USER: DEFAULT ROLE {NONE | ALL | role [, role]...}. +type UserDefaultRoles struct { + All bool + None bool + Roles []*auth.RoleIdentity +} + +// Restore implements Node interface. +func (n *UserDefaultRoles) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("DEFAULT ROLE ") + switch { + case n.All: + ctx.WriteKeyWord("ALL") + case n.None: + ctx.WriteKeyWord("NONE") + default: + for i, role := range n.Roles { + if i != 0 { + ctx.WritePlain(", ") + } + if err := role.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore UserDefaultRoles.Roles[%d]", i) + } + } + } + return nil +} + +// AlterUserFactorOpType names the multi-factor authentication operation +// of an AlterUserFactor clause. +type AlterUserFactorOpType int + +// Multi-factor authentication operations. +const ( + // AlterUserFactorAdd is ADD n FACTOR auth_option. + AlterUserFactorAdd AlterUserFactorOpType = iota + // AlterUserFactorModify is MODIFY n FACTOR auth_option. + AlterUserFactorModify + // AlterUserFactorDrop is DROP n FACTOR. + AlterUserFactorDrop + // AlterUserFactorInitiateRegistration is n FACTOR INITIATE REGISTRATION. + AlterUserFactorInitiateRegistration + // AlterUserFactorFinishRegistration is n FACTOR FINISH REGISTRATION + // SET CHALLENGE_RESPONSE AS 'auth_string'. + AlterUserFactorFinishRegistration + // AlterUserFactorUnregister is n FACTOR UNREGISTER. + AlterUserFactorUnregister +) + +// AlterUserFactor is one multi-factor authentication clause of ALTER +// USER (MySQL 26.7 §15.7.1.1): ADD/MODIFY/DROP n FACTOR and the +// registration steps. +type AlterUserFactor struct { + Op AlterUserFactorOpType + Factor int64 // 2 or 3 + // AuthOpt is set for Add and Modify. + AuthOpt *AuthOption + // ChallengeResponse is the FINISH REGISTRATION SET + // CHALLENGE_RESPONSE AS value; nil otherwise. + ChallengeResponse *string +} + +// Restore implements Node interface. +func (n *AlterUserFactor) Restore(ctx *format.RestoreCtx) error { + switch n.Op { + case AlterUserFactorAdd: + ctx.WriteKeyWord("ADD ") + case AlterUserFactorModify: + ctx.WriteKeyWord("MODIFY ") + case AlterUserFactorDrop: + ctx.WriteKeyWord("DROP ") + } + ctx.WritePlainf("%d", n.Factor) + ctx.WriteKeyWord(" FACTOR") + switch n.Op { + case AlterUserFactorAdd, AlterUserFactorModify: + ctx.WritePlain(" ") + if err := n.AuthOpt.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterUserFactor.AuthOpt") + } + case AlterUserFactorInitiateRegistration: + ctx.WriteKeyWord(" INITIATE REGISTRATION") + case AlterUserFactorFinishRegistration: + ctx.WriteKeyWord(" FINISH REGISTRATION SET CHALLENGE_RESPONSE AS ") + ctx.WriteString(*n.ChallengeResponse) + case AlterUserFactorUnregister: + ctx.WriteKeyWord(" UNREGISTER") + } + return nil +} + // CreateUserStmt creates user account. // See https://dev.mysql.com/doc/refman/8.0/en/create-user.html type CreateUserStmt struct { @@ -1970,6 +2122,8 @@ type CreateUserStmt struct { PasswordOrLockOptions []*PasswordOrLockOption CommentOrAttributeOption *CommentOrAttributeOption ResourceGroupNameOption *ResourceGroupNameOption + // DefaultRoles is the DEFAULT ROLE clause; nil when absent. + DefaultRoles *UserDefaultRoles } // Restore implements Node interface. @@ -1991,6 +2145,13 @@ func (n *CreateUserStmt) Restore(ctx *format.RestoreCtx) error { } } + if n.DefaultRoles != nil { + ctx.WritePlain(" ") + if err := n.DefaultRoles.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore CreateUserStmt.DefaultRoles") + } + } + if len(n.AuthTokenOrTLSOptions) != 0 { ctx.WriteKeyWord(" REQUIRE ") } @@ -2071,6 +2232,11 @@ type AlterUserStmt struct { PasswordOrLockOptions []*PasswordOrLockOption CommentOrAttributeOption *CommentOrAttributeOption ResourceGroupNameOption *ResourceGroupNameOption + // DefaultRoles is the DEFAULT ROLE clause; nil when absent. + DefaultRoles *UserDefaultRoles + // Factors are the multi-factor authentication clauses (ADD/MODIFY/ + // DROP n FACTOR and the registration steps); empty when absent. + Factors []*AlterUserFactor } // Restore implements Node interface. @@ -2095,6 +2261,20 @@ func (n *AlterUserStmt) Restore(ctx *format.RestoreCtx) error { } } + for i, factor := range n.Factors { + ctx.WritePlain(" ") + if err := factor.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore AlterUserStmt.Factors[%d]", i) + } + } + + if n.DefaultRoles != nil { + ctx.WritePlain(" ") + if err := n.DefaultRoles.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore AlterUserStmt.DefaultRoles") + } + } + if len(n.AuthTokenOrTLSOptions) != 0 { ctx.WriteKeyWord(" REQUIRE ") } @@ -3303,11 +3483,18 @@ type RevokeStmt struct { ObjectType ObjectTypeType Level *GrantLevel Users []*UserSpec + // IfExists is the IF EXISTS clause. + IfExists bool + // IgnoreUnknownUser is the IGNORE UNKNOWN USER clause. + IgnoreUnknownUser bool } // Restore implements Node interface. func (n *RevokeStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("REVOKE ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } for i, v := range n.Privs { if i != 0 { ctx.WritePlain(", ") @@ -3335,6 +3522,9 @@ func (n *RevokeStmt) Restore(ctx *format.RestoreCtx) error { return annotatef(err, "An error occurred while restore RevokeStmt.Users[%d]", i) } } + if n.IgnoreUnknownUser { + ctx.WriteKeyWord(" IGNORE UNKNOWN USER") + } return nil } @@ -3361,11 +3551,18 @@ type RevokeRoleStmt struct { Roles []*auth.RoleIdentity Users []*auth.UserIdentity + // IfExists is the IF EXISTS clause. + IfExists bool + // IgnoreUnknownUser is the IGNORE UNKNOWN USER clause. + IgnoreUnknownUser bool } // Restore implements Node interface. func (n *RevokeRoleStmt) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("REVOKE ") + if n.IfExists { + ctx.WriteKeyWord("IF EXISTS ") + } for i, role := range n.Roles { if i != 0 { ctx.WritePlain(", ") @@ -3383,6 +3580,9 @@ func (n *RevokeRoleStmt) Restore(ctx *format.RestoreCtx) error { return annotatef(err, "An error occurred while restore RevokeRoleStmt.Users[%d]", i) } } + if n.IgnoreUnknownUser { + ctx.WriteKeyWord(" IGNORE UNKNOWN USER") + } return nil } @@ -3406,6 +3606,53 @@ type GrantStmt struct { Users []*UserSpec AuthTokenOrTLSOptions []*AuthTokenOrTLSOption WithGrant bool + // AsUser is the AS user [WITH ROLE ...] clause; nil when absent. + AsUser *GrantAs +} + +// GrantAs is the AS user [WITH ROLE {DEFAULT | NONE | ALL | ALL EXCEPT +// role [, role]... | role [, role]...}] clause of GRANT, which +// evaluates the granted privileges with the given user's access rights. +type GrantAs struct { + User *auth.UserIdentity + // WithRole reports whether a WITH ROLE clause is present; RoleOpt + // and Roles describe it. + WithRole bool + RoleOpt SetRoleStmtType + Roles []*auth.RoleIdentity +} + +// Restore implements Node interface. +func (n *GrantAs) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("AS ") + if err := n.User.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore GrantAs.User") + } + if !n.WithRole { + return nil + } + ctx.WriteKeyWord(" WITH ROLE ") + switch n.RoleOpt { + case SetRoleDefault: + ctx.WriteKeyWord("DEFAULT") + case SetRoleNone: + ctx.WriteKeyWord("NONE") + case SetRoleAll: + ctx.WriteKeyWord("ALL") + case SetRoleAllExcept: + ctx.WriteKeyWord("ALL EXCEPT ") + } + if n.RoleOpt == SetRoleAllExcept || n.RoleOpt == SetRoleRegular { + for i, role := range n.Roles { + if i != 0 { + ctx.WritePlain(", ") + } + if err := role.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore GrantAs.Roles[%d]", i) + } + } + } + return nil } // Restore implements Node interface. @@ -3456,6 +3703,12 @@ func (n *GrantStmt) Restore(ctx *format.RestoreCtx) error { if n.WithGrant { ctx.WriteKeyWord(" WITH GRANT OPTION") } + if n.AsUser != nil { + ctx.WritePlain(" ") + if err := n.AsUser.Restore(ctx); err != nil { + return annotate(err, "An error occurred while restore GrantStmt.AsUser") + } + } return nil } @@ -3527,12 +3780,51 @@ func (n *GrantProxyStmt) Restore(ctx *format.RestoreCtx) error { return nil } +// RevokeProxyStmt is the struct for REVOKE PROXY statement: +// REVOKE PROXY ON user FROM user [, user]... +type RevokeProxyStmt struct { + stmtNode + + LocalUser *auth.UserIdentity + ExternalUsers []*auth.UserIdentity +} + +// Restore implements Node interface. +func (n *RevokeProxyStmt) Restore(ctx *format.RestoreCtx) error { + ctx.WriteKeyWord("REVOKE PROXY ON ") + if err := n.LocalUser.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore RevokeProxyStmt.LocalUser") + } + ctx.WriteKeyWord(" FROM ") + for i, v := range n.ExternalUsers { + if i != 0 { + ctx.WritePlain(", ") + } + if err := v.Restore(ctx); err != nil { + return annotatef(err, "An error occurred while restore RevokeProxyStmt.ExternalUsers[%d]", i) + } + } + return nil +} + +// Accept implements Node Accept interface. +func (n *RevokeProxyStmt) Accept(v Visitor) (Node, bool) { + newNode, skipChildren := v.Enter(n) + if skipChildren { + return v.Leave(newNode) + } + n = newNode.(*RevokeProxyStmt) + return v.Leave(n) +} + // GrantRoleStmt is the struct for GRANT TO statement. type GrantRoleStmt struct { stmtNode Roles []*auth.RoleIdentity Users []*auth.UserIdentity + // AdminOption is the WITH ADMIN OPTION clause. + AdminOption bool } // Accept implements Node Accept interface. @@ -3567,6 +3859,9 @@ func (n *GrantRoleStmt) Restore(ctx *format.RestoreCtx) error { return annotatef(err, "An error occurred while restore GrantStmt.Users[%d]", i) } } + if n.AdminOption { + ctx.WriteKeyWord(" WITH ADMIN OPTION") + } return nil } diff --git a/ast/sem.go b/ast/sem.go index 32657da..57e1668 100644 --- a/ast/sem.go +++ b/ast/sem.go @@ -451,6 +451,8 @@ const ( RestartCommand = "RESTART" // RevokeCommand represents REVOKE statement RevokeCommand = "REVOKE" + // RevokeProxyCommand represents REVOKE PROXY statement + RevokeProxyCommand = "REVOKE PROXY" // RevokeRoleCommand represents REVOKE ROLE statement RevokeRoleCommand = "REVOKE ROLE" // RollbackCommand represents ROLLBACK statement @@ -1262,6 +1264,11 @@ func (n *GrantProxyStmt) SEMCommand() string { return GrantProxyCommand } +// SEMCommand returns the command string for the statement. +func (n *RevokeProxyStmt) SEMCommand() string { + return RevokeProxyCommand +} + // SEMCommand returns the command string for the statement. func (n *GrantRoleStmt) SEMCommand() string { return GrantRoleCommand diff --git a/parser/keyword_classes.go b/parser/keyword_classes.go index 85fac08..27d69f9 100644 --- a/parser/keyword_classes.go +++ b/parser/keyword_classes.go @@ -410,6 +410,15 @@ var unReservedKeywordNames = []string{ "RETURNING", "SOUNDS", "ZONE", + "CHALLENGE_RESPONSE", + "FACTOR", + "FINISH", + "INITIATE", + "OLD", + "RANDOM", + "REGISTRATION", + "RETAIN", + "UNREGISTER", "TPCC", "OLTP_READ_WRITE", "OLTP_READ_ONLY", diff --git a/parser/keywords.go b/parser/keywords.go index 4c7593f..bd84ada 100644 --- a/parser/keywords.go +++ b/parser/keywords.go @@ -321,6 +321,7 @@ var Keywords = []KeywordsType{ {"CASCADED", false, "unreserved"}, {"CAUSAL", false, "unreserved"}, {"CHAIN", false, "unreserved"}, + {"CHALLENGE_RESPONSE", false, "unreserved"}, {"CHANGED", false, "unreserved"}, {"CHANNEL", false, "unreserved"}, {"CHARSET", false, "unreserved"}, @@ -418,12 +419,14 @@ var Keywords = []KeywordsType{ {"EXPLORE", false, "unreserved"}, {"EXPORT", false, "unreserved"}, {"EXTENDED", false, "unreserved"}, + {"FACTOR", false, "unreserved"}, {"FAILED_LOGIN_ATTEMPTS", false, "unreserved"}, {"FAST", false, "unreserved"}, {"FAULTS", false, "unreserved"}, {"FIELDS", false, "unreserved"}, {"FILE", false, "unreserved"}, {"FILTER", false, "unreserved"}, + {"FINISH", false, "unreserved"}, {"FIRST", false, "unreserved"}, {"FIXED", false, "unreserved"}, {"FLUSH", false, "unreserved"}, @@ -456,6 +459,7 @@ var Keywords = []KeywordsType{ {"INCREMENT", false, "unreserved"}, {"INCREMENTAL", false, "unreserved"}, {"INDEXES", false, "unreserved"}, + {"INITIATE", false, "unreserved"}, {"INNODB", false, "unreserved"}, {"INSERT_METHOD", false, "unreserved"}, {"INSTALL", false, "unreserved"}, @@ -532,6 +536,7 @@ var Keywords = []KeywordsType{ {"NVARCHAR", false, "unreserved"}, {"OFF", false, "unreserved"}, {"OFFSET", false, "unreserved"}, + {"OLD", false, "unreserved"}, {"OLTP_READ_ONLY", false, "unreserved"}, {"OLTP_READ_WRITE", false, "unreserved"}, {"OLTP_WRITE_ONLY", false, "unreserved"}, @@ -587,12 +592,14 @@ var Keywords = []KeywordsType{ {"QUERIES", false, "unreserved"}, {"QUERY", false, "unreserved"}, {"QUICK", false, "unreserved"}, + {"RANDOM", false, "unreserved"}, {"RATE_LIMIT", false, "unreserved"}, {"REBUILD", false, "unreserved"}, {"RECOMMEND", false, "unreserved"}, {"RECOVER", false, "unreserved"}, {"REDUNDANT", false, "unreserved"}, {"REFRESH", false, "unreserved"}, + {"REGISTRATION", false, "unreserved"}, {"RELAY", false, "unreserved"}, {"RELAYLOG", false, "unreserved"}, {"RELOAD", false, "unreserved"}, @@ -610,6 +617,7 @@ var Keywords = []KeywordsType{ {"RESTORE", false, "unreserved"}, {"RESTORES", false, "unreserved"}, {"RESUME", false, "unreserved"}, + {"RETAIN", false, "unreserved"}, {"RETURNING", false, "unreserved"}, {"RETURNS", false, "unreserved"}, {"REUSE", false, "unreserved"}, @@ -724,6 +732,7 @@ var Keywords = []KeywordsType{ {"UNICODE", false, "unreserved"}, {"UNINSTALL", false, "unreserved"}, {"UNKNOWN", false, "unreserved"}, + {"UNREGISTER", false, "unreserved"}, {"UNSET", false, "unreserved"}, {"UPGRADE", false, "unreserved"}, {"USER", false, "unreserved"}, diff --git a/parser/keywords_test.go b/parser/keywords_test.go index 6c298b8..aca386c 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(775, len(parser.Keywords)) { - t.Fatalf("got %v, want %v", len(parser.Keywords), 775) + if !reflect.DeepEqual(784, len(parser.Keywords)) { + t.Fatalf("got %v, want %v", len(parser.Keywords), 784) } reservedNr := 0 diff --git a/parser/misc.go b/parser/misc.go index 66b6286..7504473 100644 --- a/parser/misc.go +++ b/parser/misc.go @@ -245,6 +245,7 @@ var tokenMap = map[string]int{ "CAUSAL": causal, "CHAIN": chain, "CHANGE": change, + "CHALLENGE_RESPONSE": challengeResponse, "CHANGED": changed, "CHANNEL": channel, "CHAR": charType, @@ -430,6 +431,7 @@ var tokenMap = map[string]int{ "FIELDS": fields, "FILE": file, "FILTER": filter, + "FINISH": finish, "FIRST": first, "FIXED": fixed, "FLASHBACK": flashback, @@ -494,6 +496,7 @@ var tokenMap = map[string]int{ "INDEXES": indexes, "INFILE": infile, "INNER": inner, + "INITIATE": initiate, "INNODB": innodb, "INOUT": inout, "INPLACE": inplace, @@ -656,6 +659,7 @@ var tokenMap = map[string]int{ "OLTP_READ_WRITE": oltpReadWrite, "OLTP_WRITE_ONLY": oltpWriteOnly, "TPCH_10": tpch10, + "OLD": old, "ON_DUPLICATE": onDuplicate, "ON": on, "ONE": one, @@ -733,6 +737,7 @@ var tokenMap = map[string]int{ "QUERY": query, "QUERY_LIMIT": queryLimit, "QUICK": quick, + "RANDOM": random, "RANGE": rangeKwd, "RATE_LIMIT": rateLimit, "RAW": raw, @@ -750,6 +755,7 @@ var tokenMap = map[string]int{ "REFRESH": refresh, "REGEXP": regexpKwd, "REGION": region, + "REGISTRATION": registration, "REGIONS": regions, "RELAY": relay, "RELAYLOG": relaylog, @@ -795,6 +801,7 @@ var tokenMap = map[string]int{ "ROWS": rows, "RTREE": rtree, "HYPO": hypo, + "RETAIN": retain, "RESUME": resume, "RETURN": returnKwd, "RETURNING": returning, @@ -992,6 +999,7 @@ var tokenMap = map[string]int{ "UNLOCK": unlock, "UNLIMITED": unlimited, "MODERATED": moderated, + "UNREGISTER": unregister, "UNSET": unset, "UNSIGNED": unsigned, "UNTIL": until, @@ -1052,6 +1060,7 @@ var tokenMap = map[string]int{ "ZONE": zone, "ZEROFILL": zerofill, "WAIT": wait, + "FACTOR": factor, "FAILED_LOGIN_ATTEMPTS": failedLoginAttempts, "PASSWORD_LOCK_TIME": passwordLockTime, "REUSE": reuse, diff --git a/parser/parse_alter.go b/parser/parse_alter.go index 7654a6c..07fffaa 100644 --- a/parser/parse_alter.go +++ b/parser/parse_alter.go @@ -1437,6 +1437,70 @@ func (r *rdParser) parseReorganizePartitionRuleOpt() *ast.AlterTableSpec { } } +// parseAlterUserFactors implements the multi-factor authentication +// clauses of ALTER USER (MySQL 26.7 §15.7.1.1): +// +// (("ADD" | "MODIFY") NUM "FACTOR" AuthOption +// | "DROP" NUM "FACTOR" +// | NUM "FACTOR" ("INITIATE" "REGISTRATION" +// | "FINISH" "REGISTRATION" "SET" "CHALLENGE_RESPONSE" +// "AS" stringLit +// | "UNREGISTER"))* +func (r *rdParser) parseAlterUserFactors() []*ast.AlterUserFactor { + var factors []*ast.AlterUserFactor + for { + switch { + case (r.tok() == add || r.tok() == modify || r.tok() == drop) && + r.la(1) == intLit && r.la(2) == factor: + f := &ast.AlterUserFactor{} + switch r.tok() { + case add: + f.Op = ast.AlterUserFactorAdd + case modify: + f.Op = ast.AlterUserFactorModify + case drop: + f.Op = ast.AlterUserFactorDrop + } + r.advance() + f.Factor = r.parseInt64Num() + r.expect(factor) + if f.Op != ast.AlterUserFactorDrop { + f.AuthOpt = r.parseGrantAuthOption() + if f.AuthOpt == nil { + r.syntaxError() + } + } + factors = append(factors, f) + case r.tok() == intLit && r.la(1) == factor: + f := &ast.AlterUserFactor{Factor: r.parseInt64Num()} + r.expect(factor) + switch r.tok() { + case initiate: + r.advance() + r.expect(registration) + f.Op = ast.AlterUserFactorInitiateRegistration + case finish: + r.advance() + r.expect(registration) + r.expect(set) + r.expect(challengeResponse) + r.expect(as) + s := r.expect(stringLit).lit + f.Op = ast.AlterUserFactorFinishRegistration + f.ChallengeResponse = &s + case unregister: + r.advance() + f.Op = ast.AlterUserFactorUnregister + default: + r.syntaxError() + } + factors = append(factors, f) + default: + return factors + } + } +} + /**************************************AlterDatabaseStmt***************************************/ // parseAlterDatabaseStmt implements AlterDatabaseStmt: @@ -1515,15 +1579,20 @@ func (r *rdParser) parseAlterUserStmt() ast.StmtNode { CurrentAuth: auth, } } - // "ALTER" "USER" IfExists UserSpecList RequireClauseOpt ConnectionOptions - // PasswordOrLockOptions CommentOrAttributeOption ResourceGroupNameOption + // "ALTER" "USER" IfExists UserSpecList FactorClauses DefaultRoleOpt + // RequireClauseOpt ConnectionOptions PasswordOrLockOptions + // CommentOrAttributeOption ResourceGroupNameOption specs := r.parseGrantUserSpecList() + factors := r.parseAlterUserFactors() + defaultRoles := r.parseUserDefaultRolesOpt() tlsOptions := r.parseGrantRequireClauseOpt() resourceOptions := r.parseUserConnectionOptions() passwordOrLockOptions := r.parseUserPasswordOrLockOptions() ret := &ast.AlterUserStmt{ IfExists: ifExists, Specs: specs, + Factors: factors, + DefaultRoles: defaultRoles, AuthTokenOrTLSOptions: tlsOptions, ResourceOptions: resourceOptions, PasswordOrLockOptions: passwordOrLockOptions, diff --git a/parser/parse_create_misc.go b/parser/parse_create_misc.go index 6385019..2d92202 100644 --- a/parser/parse_create_misc.go +++ b/parser/parse_create_misc.go @@ -275,6 +275,7 @@ func (r *rdParser) parseCreateUserStmt() ast.StmtNode { r.expect(user) ifNotExists := r.parseIfNotExists() specs := r.parseGrantUserSpecList() + defaultRoles := r.parseUserDefaultRolesOpt() tlsOptions := r.parseGrantRequireClauseOpt() resourceOptions := r.parseUserConnectionOptions() pwdLockOptions := r.parseUserPasswordOrLockOptions() @@ -285,6 +286,7 @@ func (r *rdParser) parseCreateUserStmt() ast.StmtNode { AuthTokenOrTLSOptions: tlsOptions, ResourceOptions: resourceOptions, PasswordOrLockOptions: pwdLockOptions, + DefaultRoles: defaultRoles, } if opt := r.parseUserCommentOrAttributeOption(); opt != nil { ret.CommentOrAttributeOption = opt @@ -295,6 +297,26 @@ func (r *rdParser) parseCreateUserStmt() ast.StmtNode { return ret } +// parseUserDefaultRolesOpt implements the DEFAULT ROLE clause of +// CREATE USER and ALTER USER (nil for the empty alternative): +// empty | "DEFAULT" "ROLE" ("NONE" | "ALL" | RolenameList). +func (r *rdParser) parseUserDefaultRolesOpt() *ast.UserDefaultRoles { + if r.tok() != defaultKwd || r.la(1) != role { + return nil + } + r.advance() + r.advance() + switch r.tok() { + case all: + r.advance() + return &ast.UserDefaultRoles{All: true} + case none: + r.advance() + return &ast.UserDefaultRoles{None: true} + } + return &ast.UserDefaultRoles{Roles: r.parseRolenameList()} +} + // parseUserConnectionOptions implements ConnectionOptions and // ConnectionOptionList (empty, or "WITH" followed by one or more // options; anything but MAX_USER_CONNECTIONS draws a warning). @@ -427,11 +449,18 @@ func (r *rdParser) parseUserPasswordOrLockOption() *ast.PasswordOrLockOption { } return &ast.PasswordOrLockOption{Type: ast.PasswordExpire} case require: - // "PASSWORD" "REQUIRE" "CURRENT" "DEFAULT" + // "PASSWORD" "REQUIRE" "CURRENT" ["DEFAULT" | "OPTIONAL"] r.advance() r.expect(current) - r.expect(defaultKwd) - return &ast.PasswordOrLockOption{Type: ast.PasswordRequireCurrentDefault} + switch r.tok() { + case defaultKwd: + r.advance() + return &ast.PasswordOrLockOption{Type: ast.PasswordRequireCurrentDefault} + case optional: + r.advance() + return &ast.PasswordOrLockOption{Type: ast.PasswordRequireCurrentOptional} + } + return &ast.PasswordOrLockOption{Type: ast.PasswordRequireCurrent} } } r.syntaxError() diff --git a/parser/parse_grant.go b/parser/parse_grant.go index 705c02c..d234f2f 100644 --- a/parser/parse_grant.go +++ b/parser/parse_grant.go @@ -81,6 +81,7 @@ func (r *rdParser) parseGrantStmtFamily() ast.StmtNode { Users: users, AuthTokenOrTLSOptions: tlsOptions, WithGrant: withGrant, + AsUser: r.parseGrantAsOpt(), } case to: // GrantRoleStmt @@ -90,16 +91,62 @@ func (r *rdParser) parseGrantStmtFamily() ast.StmtNode { if err != nil { r.actionError(err) } - return &ast.GrantRoleStmt{ + stmt := &ast.GrantRoleStmt{ Roles: roles, Users: users, } + if r.tok() == with && r.la(1) == admin { + // "WITH" "ADMIN" "OPTION" + r.advance() + r.advance() + r.expect(option) + stmt.AdminOption = true + } + return stmt default: r.syntaxError() return nil } } +// parseGrantAsOpt implements the AS user [WITH ROLE ...] clause of +// GrantStmt (nil for the empty alternative): +// +// empty | "AS" Username ["WITH" "ROLE" ("DEFAULT" | "NONE" | "ALL" +// | "ALL" "EXCEPT" RolenameList | RolenameList)] +func (r *rdParser) parseGrantAsOpt() *ast.GrantAs { + if r.tok() != as { + return nil + } + r.advance() + grantAs := &ast.GrantAs{User: r.parseGrantUsername()} + if r.tok() == with && r.la(1) == role { + r.advance() + r.advance() + grantAs.WithRole = true + switch r.tok() { + case defaultKwd: + r.advance() + grantAs.RoleOpt = ast.SetRoleDefault + case none: + r.advance() + grantAs.RoleOpt = ast.SetRoleNone + case all: + r.advance() + if r.accept(except) { + grantAs.RoleOpt = ast.SetRoleAllExcept + grantAs.Roles = r.parseRolenameList() + } else { + grantAs.RoleOpt = ast.SetRoleAll + } + default: + grantAs.RoleOpt = ast.SetRoleRegular + grantAs.Roles = r.parseRolenameList() + } + } + return grantAs +} + // parseRevokeStmtFamily implements: // // RevokeStmt: "REVOKE" RoleOrPrivElemList "ON" ObjectType PrivLevel @@ -109,6 +156,18 @@ func (r *rdParser) parseGrantStmtFamily() ast.StmtNode { // disambiguated by "ON" vs "FROM" after the shared list. func (r *rdParser) parseRevokeStmtFamily() ast.StmtNode { r.expect(revoke) + ifExists := r.parseIfExists() + if r.tok() == proxy && r.la(1) == on { + // RevokeProxyStmt: "REVOKE" "PROXY" "ON" Username "FROM" UsernameList + r.advance() + r.advance() + localUser := r.parseGrantUsername() + r.expect(from) + return &ast.RevokeProxyStmt{ + LocalUser: localUser, + ExternalUsers: r.parseGrantUsernameList(), + } + } elems := r.parseRoleOrPrivElemList() switch r.tok() { case on: @@ -123,10 +182,12 @@ func (r *rdParser) parseRevokeStmtFamily() ast.StmtNode { r.actionError(err) } return &ast.RevokeStmt{ - Privs: p, - ObjectType: objectType, - Level: level, - Users: users, + Privs: p, + ObjectType: objectType, + Level: level, + Users: users, + IfExists: ifExists, + IgnoreUnknownUser: r.parseIgnoreUnknownUserOpt(), } case from: // RevokeRoleStmt @@ -145,10 +206,12 @@ func (r *rdParser) parseRevokeStmtFamily() ast.StmtNode { }) } return &ast.RevokeStmt{ - Privs: []*ast.PrivElem{{Priv: mysql.AllPriv}, {Priv: mysql.GrantPriv}}, - ObjectType: ast.ObjectTypeNone, - Level: &ast.GrantLevel{Level: ast.GrantLevelGlobal}, - Users: users, + Privs: []*ast.PrivElem{{Priv: mysql.AllPriv}, {Priv: mysql.GrantPriv}}, + ObjectType: ast.ObjectTypeNone, + Level: &ast.GrantLevel{Level: ast.GrantLevelGlobal}, + Users: users, + IfExists: ifExists, + IgnoreUnknownUser: r.parseIgnoreUnknownUserOpt(), } } roles, err := convertToRole(elems) @@ -156,8 +219,10 @@ func (r *rdParser) parseRevokeStmtFamily() ast.StmtNode { r.actionError(err) } return &ast.RevokeRoleStmt{ - Roles: roles, - Users: usernames, + Roles: roles, + Users: usernames, + IfExists: ifExists, + IgnoreUnknownUser: r.parseIgnoreUnknownUserOpt(), } default: r.syntaxError() @@ -165,6 +230,18 @@ func (r *rdParser) parseRevokeStmtFamily() ast.StmtNode { } } +// parseIgnoreUnknownUserOpt implements the IGNORE UNKNOWN USER clause of +// the REVOKE statements: empty | "IGNORE" "UNKNOWN" "USER". +func (r *rdParser) parseIgnoreUnknownUserOpt() bool { + if r.tok() != ignore { + return false + } + r.advance() + r.expect(unknown) + r.expect(user) + return true +} + // parseRoleOrPrivElemList implements RoleOrPrivElemList: // RoleOrPrivElem | RoleOrPrivElemList ',' RoleOrPrivElem. func (r *rdParser) parseRoleOrPrivElemList() []*ast.RoleOrPriv { @@ -531,8 +608,21 @@ func (r *rdParser) parseGrantUserSpec() *ast.UserSpec { userSpec := &ast.UserSpec{ User: r.parseGrantUsername(), } + if r.tok() == discard { + // "DISCARD" "OLD" "PASSWORD" replaces the IDENTIFIED clause. + r.advance() + r.expect(old) + r.expect(password) + userSpec.AuthOpt = &ast.AuthOption{DiscardOldPassword: true} + return userSpec + } if opt := r.parseGrantAuthOption(); opt != nil { userSpec.AuthOpt = opt + // Multi-factor authentication: "AND" IDENTIFIED ... up to twice. + for r.tok() == and && r.la(1) == identified { + r.advance() + userSpec.MoreAuthOpts = append(userSpec.MoreAuthOpts, r.parseGrantAuthOption()) + } } return userSpec } @@ -550,30 +640,41 @@ func (r *rdParser) parseGrantUserSpecList() []*ast.UserSpec { // parseGrantAuthOption implements AuthOption (nil for the empty // alternative): // -// empty | "IDENTIFIED" "BY" AuthString -// | "IDENTIFIED" "WITH" AuthPlugin ["BY" AuthString | "AS" HashString] +// empty | "IDENTIFIED" "BY" (AuthString | "RANDOM" "PASSWORD") +// | "IDENTIFIED" "WITH" AuthPlugin +// ["BY" (AuthString | "RANDOM" "PASSWORD") | "AS" HashString] // | "IDENTIFIED" "BY" "PASSWORD" HashString // -// with AuthString: stringLit and AuthPlugin: StringName. +// with AuthString: stringLit and AuthPlugin: StringName, followed by the +// optional dual-password clauses ["REPLACE" stringLit] ["RETAIN" +// "CURRENT" "PASSWORD"]. func (r *rdParser) parseGrantAuthOption() *ast.AuthOption { if r.tok() != identified { return nil } r.advance() + var opt *ast.AuthOption switch r.tok() { case by: r.advance() - if r.accept(password) { + switch { + case r.tok() == random: + // "BY" "RANDOM" "PASSWORD" + r.advance() + r.expect(password) + opt = &ast.AuthOption{ByRandomPassword: true} + case r.accept(password): // "IDENTIFIED" "BY" "PASSWORD" HashString - return &ast.AuthOption{ + opt = &ast.AuthOption{ AuthPlugin: mysql.AuthNativePassword, HashString: r.parseGrantHashString(), ByHashString: true, } - } - return &ast.AuthOption{ - AuthString: r.expect(stringLit).lit, - ByAuthString: true, + default: + opt = &ast.AuthOption{ + AuthString: r.expect(stringLit).lit, + ByAuthString: true, + } } case with: r.advance() @@ -581,26 +682,45 @@ func (r *rdParser) parseGrantAuthOption() *ast.AuthOption { switch r.tok() { case by: r.advance() - return &ast.AuthOption{ - AuthPlugin: plugin, - AuthString: r.expect(stringLit).lit, - ByAuthString: true, + if r.tok() == random { + r.advance() + r.expect(password) + opt = &ast.AuthOption{AuthPlugin: plugin, ByRandomPassword: true} + } else { + opt = &ast.AuthOption{ + AuthPlugin: plugin, + AuthString: r.expect(stringLit).lit, + ByAuthString: true, + } } case as: r.advance() - return &ast.AuthOption{ + opt = &ast.AuthOption{ AuthPlugin: plugin, HashString: r.parseGrantHashString(), ByHashString: true, } - } - return &ast.AuthOption{ - AuthPlugin: plugin, + default: + opt = &ast.AuthOption{ + AuthPlugin: plugin, + } } default: r.syntaxError() return nil } + if r.tok() == replace && r.la(1) == stringLit { + r.advance() + s := r.expect(stringLit).lit + opt.ReplacePassword = &s + } + if r.tok() == retain { + r.advance() + r.expect(current) + r.expect(password) + opt.RetainCurrentPassword = true + } + return opt } // parseGrantHashString implements HashString: stringLit | hexLit. diff --git a/parser/parse_set.go b/parser/parse_set.go index 5c66328..9b1cc9d 100644 --- a/parser/parse_set.go +++ b/parser/parse_set.go @@ -63,9 +63,10 @@ func (r *rdParser) parseSetStmt() ast.StmtNode { case password: // "SET" "PASSWORD" EqOrAssignmentEq PasswordOpt // "SET" "PASSWORD" "FOR" Username EqOrAssignmentEq PasswordOpt + // "SET" "PASSWORD" ["FOR" Username] "TO" "RANDOM" // The LALR machine shifts toward these on "="/":="/FOR (shift // beats reducing PASSWORD to Identifier). - if r.la(1) == forKwd || r.la(1) == eq || r.la(1) == assignmentEq { + if r.la(1) == forKwd || r.la(1) == eq || r.la(1) == assignmentEq || r.la(1) == to { return r.parseSetPwdStmt() } case global, session: @@ -132,13 +133,32 @@ func (r *rdParser) parseSetStmt() ast.StmtNode { // (SET already consumed). func (r *rdParser) parseSetPwdStmt() ast.StmtNode { r.expect(password) + stmt := &ast.SetPwdStmt{} if r.accept(forKwd) { - user := r.parseUsername() + stmt.User = r.parseUsername() + } + if r.tok() == to { + // "TO" "RANDOM" + r.advance() + r.expect(random) + stmt.ToRandom = true + } else { r.parseEqOrAssignmentEq() - return &ast.SetPwdStmt{User: user, Password: r.parsePasswordOpt()} + stmt.Password = r.parsePasswordOpt() } - r.parseEqOrAssignmentEq() - return &ast.SetPwdStmt{Password: r.parsePasswordOpt()} + // The dual-password clauses. + if r.tok() == replace && r.la(1) == stringLit { + r.advance() + s := r.expect(stringLit).lit + stmt.ReplacePassword = &s + } + if r.tok() == retain { + r.advance() + r.expect(current) + r.expect(password) + stmt.RetainCurrentPassword = true + } + return stmt } // parseSetConfigStmt implements the SET CONFIG alternatives of SetStmt diff --git a/parser/testdata/parser/mysql_unsupported_account/output.sql b/parser/testdata/parser/mysql_unsupported_account/output.sql index e38570c..bf6c51c 100644 --- a/parser/testdata/parser/mysql_unsupported_account/output.sql +++ b/parser/testdata/parser/mysql_unsupported_account/output.sql @@ -1,75 +1,75 @@ --- error: line 1 column 34 near "RANDOM PASSWORD" +CREATE USER `u`@`%` IDENTIFIED BY RANDOM PASSWORD -- case --- error: line 1 column 61 near "RANDOM PASSWORD" +CREATE USER `u`@`%` IDENTIFIED WITH 'caching_sha2_password' BY RANDOM PASSWORD -- case --- error: line 1 column 33 near "RANDOM PASSWORD" +ALTER USER `u`@`%` IDENTIFIED BY RANDOM PASSWORD -- case --- error: line 1 column 33 near "RANDOM PASSWORD RETAIN CURRENT PASSWORD" +ALTER USER `u`@`%` IDENTIFIED BY RANDOM PASSWORD RETAIN CURRENT PASSWORD -- case --- error: line 1 column 36 near "AND IDENTIFIED WITH authentication_ldap_simple" +CREATE USER `u`@`%` IDENTIFIED BY 'p1' AND IDENTIFIED WITH 'authentication_ldap_simple' -- case --- error: line 1 column 36 near "AND IDENTIFIED WITH authentication_fido AND IDENTIFIED WITH authentication_ldap_simple BY 'p3'" +CREATE USER `u`@`%` IDENTIFIED BY 'p1' AND IDENTIFIED WITH 'authentication_fido' AND IDENTIFIED WITH 'authentication_ldap_simple' BY 'p3' -- case --- error: line 1 column 16 near "ADD 2 FACTOR IDENTIFIED WITH authentication_ldap_simple" +ALTER USER `u`@`%` ADD 2 FACTOR IDENTIFIED WITH 'authentication_ldap_simple' -- case --- error: line 1 column 19 near "MODIFY 2 FACTOR IDENTIFIED WITH authentication_ldap_simple BY 'p'" +ALTER USER `u`@`%` MODIFY 2 FACTOR IDENTIFIED WITH 'authentication_ldap_simple' BY 'p' -- case --- error: line 1 column 17 near "DROP 2 FACTOR" +ALTER USER `u`@`%` DROP 2 FACTOR -- case --- error: line 1 column 14 near "2 FACTOR INITIATE REGISTRATION" +ALTER USER `u`@`%` 2 FACTOR INITIATE REGISTRATION -- case --- error: line 1 column 14 near "2 FACTOR FINISH REGISTRATION SET CHALLENGE_RESPONSE AS 'blob'" +ALTER USER `u`@`%` 2 FACTOR FINISH REGISTRATION SET CHALLENGE_RESPONSE AS 'blob' -- case --- error: line 1 column 14 near "2 FACTOR UNREGISTER" +ALTER USER `u`@`%` 2 FACTOR UNREGISTER -- case --- error: line 1 column 21 near "DEFAULT ROLE r1, r2" +CREATE USER `u`@`%` DEFAULT ROLE `r1`@`%`, `r2`@`%` -- case --- error: line 1 column 38 near "" +CREATE USER `u`@`%` PASSWORD REQUIRE CURRENT -- case --- error: line 1 column 47 near "OPTIONAL" +CREATE USER `u`@`%` PASSWORD REQUIRE CURRENT OPTIONAL -- case --- error: line 1 column 38 near "REPLACE 'old'" +ALTER USER `u`@`%` IDENTIFIED BY 'p' REPLACE 'old' -- case --- error: line 1 column 37 near "RETAIN CURRENT PASSWORD" +ALTER USER `u`@`%` IDENTIFIED BY 'p' RETAIN CURRENT PASSWORD -- case --- error: line 1 column 38 near "REPLACE 'old' RETAIN CURRENT PASSWORD" +ALTER USER `u`@`%` IDENTIFIED BY 'p' REPLACE 'old' RETAIN CURRENT PASSWORD -- case --- error: line 1 column 20 near "DISCARD OLD PASSWORD" +ALTER USER `u`@`%` DISCARD OLD PASSWORD -- case --- error: line 1 column 20 near "DEFAULT ROLE ALL" +ALTER USER `u`@`%` DEFAULT ROLE ALL -- case --- error: line 1 column 20 near "DEFAULT ROLE NONE" +ALTER USER `u`@`%` DEFAULT ROLE NONE -- case --- error: line 1 column 20 near "DEFAULT ROLE r1, r2" +ALTER USER `u`@`%` DEFAULT ROLE `r1`@`%`, `r2`@`%` -- case --- error: line 1 column 18 near "WITH ADMIN OPTION" +GRANT `r1`@`%` TO `u`@`%` WITH ADMIN OPTION -- case --- error: line 1 column 27 near "AS root@localhost" +GRANT SELECT ON *.* TO `u`@`%` AS `root`@`localhost` -- case --- error: line 1 column 27 near "AS root@localhost WITH ROLE DEFAULT" +GRANT SELECT ON *.* TO `u`@`%` AS `root`@`localhost` WITH ROLE DEFAULT -- case --- error: line 1 column 27 near "AS root@localhost WITH ROLE NONE" +GRANT SELECT ON *.* TO `u`@`%` AS `root`@`localhost` WITH ROLE NONE -- case --- error: line 1 column 27 near "AS root@localhost WITH ROLE ALL" +GRANT SELECT ON *.* TO `u`@`%` AS `root`@`localhost` WITH ROLE ALL -- case --- error: line 1 column 27 near "AS root@localhost WITH ROLE ALL EXCEPT r1, r2" +GRANT SELECT ON *.* TO `u`@`%` AS `root`@`localhost` WITH ROLE ALL EXCEPT `r1`@`%`, `r2`@`%` -- case --- error: line 1 column 27 near "AS root@localhost WITH ROLE r1, r2" +GRANT SELECT ON *.* TO `u`@`%` AS `root`@`localhost` WITH ROLE `r1`@`%`, `r2`@`%` -- case --- error: line 1 column 9 near "IF EXISTS SELECT ON db.t FROM u" +REVOKE IF EXISTS SELECT ON `db`.`t` FROM `u`@`%` -- case --- error: line 1 column 35 near "IGNORE UNKNOWN USER" +REVOKE SELECT ON `db`.`t` FROM `u`@`%` IGNORE UNKNOWN USER -- case --- error: line 1 column 9 near "IF EXISTS SELECT ON db.t FROM u IGNORE UNKNOWN USER" +REVOKE IF EXISTS SELECT ON `db`.`t` FROM `u`@`%` IGNORE UNKNOWN USER -- case --- error: line 1 column 12 near "PROXY ON pu FROM u" +REVOKE PROXY ON `pu`@`%` FROM `u`@`%` -- case --- error: line 1 column 26 near "REPLACE 'old'" +SET PASSWORD='p' REPLACE 'old' -- case --- error: line 1 column 31 near "RETAIN CURRENT PASSWORD" +SET PASSWORD FOR `u`@`%`='p' RETAIN CURRENT PASSWORD -- case --- error: line 1 column 15 near "TO RANDOM" +SET PASSWORD TO RANDOM -- case --- error: line 1 column 21 near "TO RANDOM" +SET PASSWORD FOR `u`@`%` TO RANDOM -- case --- error: line 1 column 21 near "TO RANDOM REPLACE 'old'" +SET PASSWORD FOR `u`@`%` TO RANDOM REPLACE 'old' diff --git a/parser/token_kinds.go b/parser/token_kinds.go index c9e3560..a64beaf 100644 --- a/parser/token_kinds.go +++ b/parser/token_kinds.go @@ -1040,6 +1040,15 @@ const ( ordinality = 58341 path = 58342 returning = 58343 + challengeResponse = 58344 + factor = 58345 + finish = 58346 + initiate = 58347 + random = 58348 + registration = 58349 + retain = 58350 + unregister = 58351 + old = 58352 wrapper = 58319 write = 57592 x509 = 57993 From ddd0cf9571bb4db9a4c079cc7647f809bd1ca710 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 13:55:49 +0000 Subject: [PATCH 13/13] Fold the mysql_unsupported_* groups into the mysql_* coverage groups With every case now supported, the mysql_unsupported_* names no longer describe their contents. Following the #39-#41 precedent of renaming an implemented group, the seven groups whose mysql_* counterpart already exists (txn, dml, ddl, admin, show, replication, compound) have their cases appended to it, and the five without one (types, functions, routines, account, utility) are renamed to mysql_. No case or golden changes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GBD2pnzaVgeXtHgWwUqxUg --- .../input.sql | 0 .../output.sql | 0 parser/testdata/parser/mysql_admin/input.sql | 36 +++++++++++++++++++ parser/testdata/parser/mysql_admin/output.sql | 36 +++++++++++++++++++ .../testdata/parser/mysql_compound/input.sql | 10 ++++++ .../testdata/parser/mysql_compound/output.sql | 10 ++++++ parser/testdata/parser/mysql_ddl/input.sql | 14 ++++++++ parser/testdata/parser/mysql_ddl/output.sql | 14 ++++++++ parser/testdata/parser/mysql_dml/input.sql | 16 +++++++++ parser/testdata/parser/mysql_dml/output.sql | 16 +++++++++ .../input.sql | 0 .../output.sql | 0 .../parser/mysql_replication/input.sql | 22 ++++++++++++ .../parser/mysql_replication/output.sql | 22 ++++++++++++ .../input.sql | 0 .../output.sql | 0 parser/testdata/parser/mysql_show/input.sql | 14 ++++++++ parser/testdata/parser/mysql_show/output.sql | 14 ++++++++ parser/testdata/parser/mysql_txn/input.sql | 16 +++++++++ parser/testdata/parser/mysql_txn/output.sql | 16 +++++++++ .../input.sql | 0 .../output.sql | 0 .../parser/mysql_unsupported_admin/input.sql | 35 ------------------ .../parser/mysql_unsupported_admin/output.sql | 35 ------------------ .../mysql_unsupported_compound/input.sql | 9 ----- .../mysql_unsupported_compound/output.sql | 9 ----- .../parser/mysql_unsupported_ddl/input.sql | 13 ------- .../parser/mysql_unsupported_ddl/output.sql | 13 ------- .../parser/mysql_unsupported_dml/input.sql | 15 -------- .../parser/mysql_unsupported_dml/output.sql | 15 -------- .../mysql_unsupported_replication/input.sql | 21 ----------- .../mysql_unsupported_replication/output.sql | 21 ----------- .../parser/mysql_unsupported_show/input.sql | 13 ------- .../parser/mysql_unsupported_show/output.sql | 13 ------- .../parser/mysql_unsupported_txn/input.sql | 15 -------- .../parser/mysql_unsupported_txn/output.sql | 15 -------- .../input.sql | 0 .../output.sql | 0 38 files changed, 256 insertions(+), 242 deletions(-) rename parser/testdata/parser/{mysql_unsupported_account => mysql_account}/input.sql (100%) rename parser/testdata/parser/{mysql_unsupported_account => mysql_account}/output.sql (100%) rename parser/testdata/parser/{mysql_unsupported_functions => mysql_functions}/input.sql (100%) rename parser/testdata/parser/{mysql_unsupported_functions => mysql_functions}/output.sql (100%) rename parser/testdata/parser/{mysql_unsupported_routines => mysql_routines}/input.sql (100%) rename parser/testdata/parser/{mysql_unsupported_routines => mysql_routines}/output.sql (100%) rename parser/testdata/parser/{mysql_unsupported_types => mysql_types}/input.sql (100%) rename parser/testdata/parser/{mysql_unsupported_types => mysql_types}/output.sql (100%) delete mode 100644 parser/testdata/parser/mysql_unsupported_admin/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_admin/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_compound/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_compound/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_ddl/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_ddl/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_dml/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_dml/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_replication/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_replication/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_show/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_show/output.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_txn/input.sql delete mode 100644 parser/testdata/parser/mysql_unsupported_txn/output.sql rename parser/testdata/parser/{mysql_unsupported_utility => mysql_utility}/input.sql (100%) rename parser/testdata/parser/{mysql_unsupported_utility => mysql_utility}/output.sql (100%) diff --git a/parser/testdata/parser/mysql_unsupported_account/input.sql b/parser/testdata/parser/mysql_account/input.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_account/input.sql rename to parser/testdata/parser/mysql_account/input.sql diff --git a/parser/testdata/parser/mysql_unsupported_account/output.sql b/parser/testdata/parser/mysql_account/output.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_account/output.sql rename to parser/testdata/parser/mysql_account/output.sql diff --git a/parser/testdata/parser/mysql_admin/input.sql b/parser/testdata/parser/mysql_admin/input.sql index 7d5651a..1ff377c 100644 --- a/parser/testdata/parser/mysql_admin/input.sql +++ b/parser/testdata/parser/mysql_admin/input.sql @@ -73,3 +73,39 @@ RESET PERSIST RESET PERSIST var1 -- case RESET PERSIST IF EXISTS var1 +-- case +ANALYZE TABLE t UPDATE HISTOGRAM ON c1 USING DATA 'json' +-- case +ANALYZE TABLE t UPDATE HISTOGRAM ON c1 AUTO UPDATE +-- case +ANALYZE TABLE t UPDATE HISTOGRAM ON c1 MANUAL UPDATE +-- case +ANALYZE TABLE t UPDATE HISTOGRAM ON c1 WITH 20 BUCKETS AUTO UPDATE +-- case +INSTALL COMPONENT 'file://c' SET PERSIST v1 = 1 +-- case +DROP RESOURCE GROUP rg FORCE +-- case +SET RESOURCE GROUP rg FOR 4 +-- case +SET RESOURCE GROUP rg FOR 4, 5, 6 +-- case +FLUSH OPTIMIZER_COSTS +-- case +FLUSH RELAY LOGS +-- case +FLUSH RELAY LOGS FOR CHANNEL 'ch' +-- case +FLUSH USER_RESOURCES +-- case +FLUSH TABLES t1, t2 FOR EXPORT +-- case +FLUSH BINARY LOGS, ERROR LOGS, STATUS +-- case +SET PERSIST max_connections = 200 +-- case +SET PERSIST_ONLY max_connections = 250 +-- case +SET PERSIST max_connections = 200, long_query_time = 0.5 +-- case +SET GLOBAL max_connections = 200, PERSIST long_query_time = 1 diff --git a/parser/testdata/parser/mysql_admin/output.sql b/parser/testdata/parser/mysql_admin/output.sql index 5cb7d6b..8c0ea00 100644 --- a/parser/testdata/parser/mysql_admin/output.sql +++ b/parser/testdata/parser/mysql_admin/output.sql @@ -73,3 +73,39 @@ RESET PERSIST RESET PERSIST `var1` -- case RESET PERSIST IF EXISTS `var1` +-- case +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` USING DATA 'json' +-- case +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` AUTO UPDATE +-- case +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` MANUAL UPDATE +-- case +ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` WITH 20 BUCKETS AUTO UPDATE +-- case +INSTALL COMPONENT 'file://c' SET @@PERSIST.`v1`=1 +-- case +DROP RESOURCE GROUP `rg` FORCE +-- case +SET RESOURCE GROUP `rg` FOR 4 +-- case +SET RESOURCE GROUP `rg` FOR 4, 5, 6 +-- case +FLUSH OPTIMIZER_COSTS +-- case +FLUSH RELAY LOGS +-- case +FLUSH RELAY LOGS FOR CHANNEL 'ch' +-- case +FLUSH USER_RESOURCES +-- case +FLUSH TABLES `t1`, `t2` FOR EXPORT +-- case +FLUSH BINARY LOGS, ERROR LOGS, STATUS +-- case +SET @@PERSIST.`max_connections`=200 +-- case +SET @@PERSIST_ONLY.`max_connections`=250 +-- case +SET @@PERSIST.`max_connections`=200, @@SESSION.`long_query_time`=0.5 +-- case +SET @@GLOBAL.`max_connections`=200, @@PERSIST.`long_query_time`=1 diff --git a/parser/testdata/parser/mysql_compound/input.sql b/parser/testdata/parser/mysql_compound/input.sql index 249945e..9a72419 100644 --- a/parser/testdata/parser/mysql_compound/input.sql +++ b/parser/testdata/parser/mysql_compound/input.sql @@ -37,3 +37,13 @@ SIGNAL SIGNAL SQLSTATE '45000' SET BOGUS_ITEM = 'x' -- case GET DIAGNOSTICS @n = MESSAGE_TEXT +-- case +CREATE PROCEDURE p () BEGIN DECLARE e CONDITION FOR SQLSTATE '23000'; DECLARE EXIT HANDLER FOR e ROLLBACK; END +-- case +CREATE PROCEDURE p () BEGIN DECLARE e CONDITION FOR 1051; END +-- case +CREATE PROCEDURE p () LOOP SET @x = 1; END LOOP +-- case +CREATE PROCEDURE p () BEGIN LOOP SET @x = 1; END LOOP; END +-- case +CREATE PROCEDURE p () lbl: LOOP LEAVE lbl; END LOOP lbl diff --git a/parser/testdata/parser/mysql_compound/output.sql b/parser/testdata/parser/mysql_compound/output.sql index 5b4fc4b..a8d027a 100644 --- a/parser/testdata/parser/mysql_compound/output.sql +++ b/parser/testdata/parser/mysql_compound/output.sql @@ -37,3 +37,13 @@ CREATE PROCEDURE `p`() BEGIN IF @`x`>1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_ -- error: line 1 column 38 near "BOGUS_ITEM = 'x'" -- case -- error: line 1 column 33 near "MESSAGE_TEXT" +-- case +CREATE PROCEDURE `p`() BEGIN DECLARE `e` CONDITION FOR SQLSTATE '23000';DECLARE EXIT HANDLER FOR `e` ROLLBACK; END +-- case +CREATE PROCEDURE `p`() BEGIN DECLARE `e` CONDITION FOR 1051; END +-- case +CREATE PROCEDURE `p`() LOOP SET @`x`=1;END LOOP +-- case +CREATE PROCEDURE `p`() BEGIN LOOP SET @`x`=1;END LOOP; END +-- case +CREATE PROCEDURE `p`() `lbl`: LOOP LEAVE `lbl`;END LOOP `lbl` diff --git a/parser/testdata/parser/mysql_ddl/input.sql b/parser/testdata/parser/mysql_ddl/input.sql index 123769b..c0b4cb5 100644 --- a/parser/testdata/parser/mysql_ddl/input.sql +++ b/parser/testdata/parser/mysql_ddl/input.sql @@ -145,3 +145,17 @@ DROP MASKING POLICY p DROP MASKING POLICY IF EXISTS p -- case SELECT `at`, `every`, `starts`, `ends`, `server`, `options`, `wrapper`, `contains`, `duality`, `rotate`, `innodb` FROM t +-- case +CREATE TABLE t (a INT) AUTOEXTEND_SIZE = 4194304 +-- case +CREATE TABLE t (a INT) START TRANSACTION +-- case +CREATE INDEX i1 ON t (a) ENGINE_ATTRIBUTE = '{}' +-- case +ALTER DATABASE d READ ONLY = 1 +-- case +ALTER DATABASE d READ ONLY = DEFAULT +-- case +CREATE VIEW v AS SELECT 1 WITH CHECK OPTION +-- case +ALTER VIEW v AS SELECT 1 WITH CHECK OPTION diff --git a/parser/testdata/parser/mysql_ddl/output.sql b/parser/testdata/parser/mysql_ddl/output.sql index ab38616..419952f 100644 --- a/parser/testdata/parser/mysql_ddl/output.sql +++ b/parser/testdata/parser/mysql_ddl/output.sql @@ -145,3 +145,17 @@ DROP MASKING POLICY `p` DROP MASKING POLICY IF EXISTS `p` -- case SELECT `at`,`every`,`starts`,`ends`,`server`,`options`,`wrapper`,`contains`,`duality`,`rotate`,`innodb` FROM `t` +-- case +CREATE TABLE `t` (`a` INT) AUTOEXTEND_SIZE = 4194304 +-- case +CREATE TABLE `t` (`a` INT) START TRANSACTION +-- case +CREATE INDEX `i1` ON `t` (`a`) ENGINE_ATTRIBUTE = '{}' +-- case +ALTER DATABASE `d` READ ONLY = 1 +-- case +ALTER DATABASE `d` READ ONLY = DEFAULT +-- case +CREATE ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 +-- case +ALTER ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 diff --git a/parser/testdata/parser/mysql_dml/input.sql b/parser/testdata/parser/mysql_dml/input.sql index fd5860b..30792fa 100644 --- a/parser/testdata/parser/mysql_dml/input.sql +++ b/parser/testdata/parser/mysql_dml/input.sql @@ -49,3 +49,19 @@ SELECT a, b INTO @x, @y FROM t LIMIT 1 SELECT 1 INTO DUMPFILE '/tmp/out' -- case SELECT prev, xml, dumpfile FROM concurrent +-- case +SELECT a INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FROM t +-- case +SELECT a FROM t INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' +-- case +SELECT * FROM t1, t2 FOR SHARE OF t1 NOWAIT FOR UPDATE OF t2 SKIP LOCKED +-- case +SELECT * FROM t INTO @a FOR UPDATE +-- case +SELECT * FROM (VALUES ROW(1, 2), ROW(3, 4)) AS v (c1, c2) +-- case +LOAD DATA CONCURRENT INFILE '/tmp/f' IGNORE INTO TABLE t +-- case +LOAD DATA INFILE '/tmp/f' INTO TABLE t PARTITION (p0) CHARACTER SET utf8mb4 +-- case +LOAD DATA INFILE '/tmp/f' INTO TABLE t COLUMNS TERMINATED BY ',' IGNORE 2 ROWS diff --git a/parser/testdata/parser/mysql_dml/output.sql b/parser/testdata/parser/mysql_dml/output.sql index 25d1729..0b5ea12 100644 --- a/parser/testdata/parser/mysql_dml/output.sql +++ b/parser/testdata/parser/mysql_dml/output.sql @@ -49,3 +49,19 @@ SELECT `a`,`b` FROM `t` LIMIT 1 INTO @`x`, @`y` SELECT 1 INTO DUMPFILE '/tmp/out' -- case SELECT `prev`,`xml`,`dumpfile` FROM `concurrent` +-- case +SELECT `a` FROM `t` INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 +-- case +SELECT `a` FROM `t` INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' +-- case +SELECT * FROM (`t1`) JOIN `t2` FOR SHARE OF `t1` NOWAIT FOR UPDATE OF `t2` SKIP LOCKED +-- case +SELECT * FROM `t` FOR UPDATE INTO @`a` +-- case +SELECT * FROM (VALUES ROW(1,2), ROW(3,4)) AS `v`(`c1`, `c2`) +-- case +LOAD DATA CONCURRENT INFILE '/tmp/f' IGNORE INTO TABLE `t` +-- case +LOAD DATA INFILE '/tmp/f' INTO TABLE `t` PARTITION (`p0`) CHARACTER SET utf8mb4 +-- case +LOAD DATA INFILE '/tmp/f' INTO TABLE `t` FIELDS TERMINATED BY ',' IGNORE 2 LINES diff --git a/parser/testdata/parser/mysql_unsupported_functions/input.sql b/parser/testdata/parser/mysql_functions/input.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_functions/input.sql rename to parser/testdata/parser/mysql_functions/input.sql diff --git a/parser/testdata/parser/mysql_unsupported_functions/output.sql b/parser/testdata/parser/mysql_functions/output.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_functions/output.sql rename to parser/testdata/parser/mysql_functions/output.sql diff --git a/parser/testdata/parser/mysql_replication/input.sql b/parser/testdata/parser/mysql_replication/input.sql index 4a59db3..f334704 100644 --- a/parser/testdata/parser/mysql_replication/input.sql +++ b/parser/testdata/parser/mysql_replication/input.sql @@ -61,3 +61,25 @@ START GROUP_REPLICATION USER = 'u', PASSWORD = 'p', DEFAULT_AUTH = 'auth_plugin' STOP GROUP_REPLICATION -- case SELECT filter, gtids, io_thread, sql_thread FROM group_replication +-- case +CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = (1, 2) +-- case +CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = () +-- case +CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = 'u'@'h' +-- case +CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = NULL +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = ON +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = OFF +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE +-- case +CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = OFF +-- case +CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = LOCAL +-- case +RESET BINARY LOGS AND GTIDS TO 100 diff --git a/parser/testdata/parser/mysql_replication/output.sql b/parser/testdata/parser/mysql_replication/output.sql index 1b70d41..60693fe 100644 --- a/parser/testdata/parser/mysql_replication/output.sql +++ b/parser/testdata/parser/mysql_replication/output.sql @@ -61,3 +61,25 @@ START GROUP_REPLICATION USER = 'u', PASSWORD = 'p', DEFAULT_AUTH = 'auth_plugin' STOP GROUP_REPLICATION -- case SELECT `filter`,`gtids`,`io_thread`,`sql_thread` FROM `group_replication` +-- case +CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = (1, 2) +-- case +CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = () +-- case +CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = `u`@`h` +-- case +CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = NULL +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = ON +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = OFF +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM +-- case +CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE +-- case +CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = OFF +-- case +CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = LOCAL +-- case +RESET BINARY LOGS AND GTIDS TO 100 diff --git a/parser/testdata/parser/mysql_unsupported_routines/input.sql b/parser/testdata/parser/mysql_routines/input.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_routines/input.sql rename to parser/testdata/parser/mysql_routines/input.sql diff --git a/parser/testdata/parser/mysql_unsupported_routines/output.sql b/parser/testdata/parser/mysql_routines/output.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_routines/output.sql rename to parser/testdata/parser/mysql_routines/output.sql diff --git a/parser/testdata/parser/mysql_show/input.sql b/parser/testdata/parser/mysql_show/input.sql index a0363f2..a8dc6f4 100644 --- a/parser/testdata/parser/mysql_show/input.sql +++ b/parser/testdata/parser/mysql_show/input.sql @@ -77,3 +77,17 @@ SHOW PARSE_TREE WITH cte AS (SELECT 1) SELECT * FROM cte SHOW PARSE_TREE UPDATE t SET a = 1 -- case SHOW MASTER LOGS +-- case +SHOW STORAGE ENGINES +-- case +SHOW ERRORS LIMIT 5 +-- case +SHOW ERRORS LIMIT 5, 10 +-- case +SHOW WARNINGS LIMIT 1 +-- case +SHOW WARNINGS LIMIT 5, 10 +-- case +SHOW EXTENDED INDEX FROM t +-- case +SHOW REPLICA STATUS FOR CHANNEL 'ch' diff --git a/parser/testdata/parser/mysql_show/output.sql b/parser/testdata/parser/mysql_show/output.sql index 5b09963..8215d89 100644 --- a/parser/testdata/parser/mysql_show/output.sql +++ b/parser/testdata/parser/mysql_show/output.sql @@ -77,3 +77,17 @@ SHOW PARSE_TREE WITH `cte` AS (SELECT 1) SELECT * FROM `cte` -- error: line 1 column 22 near "UPDATE t SET a = 1" -- case -- error: line 1 column 16 near "LOGS" +-- case +SHOW ENGINES +-- case +SHOW ERRORS LIMIT 5 +-- case +SHOW ERRORS LIMIT 5,10 +-- case +SHOW WARNINGS LIMIT 1 +-- case +SHOW WARNINGS LIMIT 5,10 +-- case +SHOW EXTENDED INDEX IN `t` +-- case +SHOW REPLICA STATUS FOR CHANNEL 'ch' diff --git a/parser/testdata/parser/mysql_txn/input.sql b/parser/testdata/parser/mysql_txn/input.sql index 4ba4c8f..9adc05f 100644 --- a/parser/testdata/parser/mysql_txn/input.sql +++ b/parser/testdata/parser/mysql_txn/input.sql @@ -35,3 +35,19 @@ LOCK INSTANCE FOR BACKUP UNLOCK INSTANCE -- case SELECT xa, xid, one, phase FROM suspend WHERE migrate = 1 +-- case +START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY +-- case +BEGIN WORK +-- case +COMMIT WORK +-- case +ROLLBACK WORK AND NO CHAIN RELEASE +-- case +ROLLBACK WORK TO s1 +-- case +LOCK TABLES t1 AS a1 WRITE +-- case +LOCK TABLES t1 a1 READ +-- case +LOCK TABLES t2 LOW_PRIORITY WRITE diff --git a/parser/testdata/parser/mysql_txn/output.sql b/parser/testdata/parser/mysql_txn/output.sql index 8409825..6b218ca 100644 --- a/parser/testdata/parser/mysql_txn/output.sql +++ b/parser/testdata/parser/mysql_txn/output.sql @@ -35,3 +35,19 @@ LOCK INSTANCE FOR BACKUP UNLOCK INSTANCE -- case SELECT `xa`,`xid`,`one`,`phase` FROM `suspend` WHERE `migrate`=1 +-- case +START TRANSACTION READ ONLY +-- case +START TRANSACTION +-- case +COMMIT +-- case +ROLLBACK RELEASE +-- case +ROLLBACK TO s1 +-- case +LOCK TABLES `t1` AS `a1` WRITE +-- case +LOCK TABLES `t1` AS `a1` READ +-- case +LOCK TABLES `t2` LOW_PRIORITY WRITE diff --git a/parser/testdata/parser/mysql_unsupported_types/input.sql b/parser/testdata/parser/mysql_types/input.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_types/input.sql rename to parser/testdata/parser/mysql_types/input.sql diff --git a/parser/testdata/parser/mysql_unsupported_types/output.sql b/parser/testdata/parser/mysql_types/output.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_types/output.sql rename to parser/testdata/parser/mysql_types/output.sql diff --git a/parser/testdata/parser/mysql_unsupported_admin/input.sql b/parser/testdata/parser/mysql_unsupported_admin/input.sql deleted file mode 100644 index d220f3a..0000000 --- a/parser/testdata/parser/mysql_unsupported_admin/input.sql +++ /dev/null @@ -1,35 +0,0 @@ -ANALYZE TABLE t UPDATE HISTOGRAM ON c1 USING DATA 'json' --- case -ANALYZE TABLE t UPDATE HISTOGRAM ON c1 AUTO UPDATE --- case -ANALYZE TABLE t UPDATE HISTOGRAM ON c1 MANUAL UPDATE --- case -ANALYZE TABLE t UPDATE HISTOGRAM ON c1 WITH 20 BUCKETS AUTO UPDATE --- case -INSTALL COMPONENT 'file://c' SET PERSIST v1 = 1 --- case -DROP RESOURCE GROUP rg FORCE --- case -SET RESOURCE GROUP rg FOR 4 --- case -SET RESOURCE GROUP rg FOR 4, 5, 6 --- case -FLUSH OPTIMIZER_COSTS --- case -FLUSH RELAY LOGS --- case -FLUSH RELAY LOGS FOR CHANNEL 'ch' --- case -FLUSH USER_RESOURCES --- case -FLUSH TABLES t1, t2 FOR EXPORT --- case -FLUSH BINARY LOGS, ERROR LOGS, STATUS --- case -SET PERSIST max_connections = 200 --- case -SET PERSIST_ONLY max_connections = 250 --- case -SET PERSIST max_connections = 200, long_query_time = 0.5 --- case -SET GLOBAL max_connections = 200, PERSIST long_query_time = 1 diff --git a/parser/testdata/parser/mysql_unsupported_admin/output.sql b/parser/testdata/parser/mysql_unsupported_admin/output.sql deleted file mode 100644 index 78b3165..0000000 --- a/parser/testdata/parser/mysql_unsupported_admin/output.sql +++ /dev/null @@ -1,35 +0,0 @@ -ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` USING DATA 'json' --- case -ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` AUTO UPDATE --- case -ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` MANUAL UPDATE --- case -ANALYZE TABLE `t` UPDATE HISTOGRAM ON `c1` WITH 20 BUCKETS AUTO UPDATE --- case -INSTALL COMPONENT 'file://c' SET @@PERSIST.`v1`=1 --- case -DROP RESOURCE GROUP `rg` FORCE --- case -SET RESOURCE GROUP `rg` FOR 4 --- case -SET RESOURCE GROUP `rg` FOR 4, 5, 6 --- case -FLUSH OPTIMIZER_COSTS --- case -FLUSH RELAY LOGS --- case -FLUSH RELAY LOGS FOR CHANNEL 'ch' --- case -FLUSH USER_RESOURCES --- case -FLUSH TABLES `t1`, `t2` FOR EXPORT --- case -FLUSH BINARY LOGS, ERROR LOGS, STATUS --- case -SET @@PERSIST.`max_connections`=200 --- case -SET @@PERSIST_ONLY.`max_connections`=250 --- case -SET @@PERSIST.`max_connections`=200, @@SESSION.`long_query_time`=0.5 --- case -SET @@GLOBAL.`max_connections`=200, @@PERSIST.`long_query_time`=1 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 0ed938e..0000000 --- a/parser/testdata/parser/mysql_unsupported_compound/input.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE PROCEDURE p () BEGIN DECLARE e CONDITION FOR SQLSTATE '23000'; DECLARE EXIT HANDLER FOR e ROLLBACK; END --- case -CREATE PROCEDURE p () BEGIN DECLARE e CONDITION FOR 1051; END --- case -CREATE PROCEDURE p () LOOP SET @x = 1; END LOOP --- case -CREATE PROCEDURE p () BEGIN LOOP SET @x = 1; END LOOP; END --- case -CREATE PROCEDURE p () lbl: LOOP LEAVE lbl; END LOOP lbl 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 8d205b6..0000000 --- a/parser/testdata/parser/mysql_unsupported_compound/output.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE PROCEDURE `p`() BEGIN DECLARE `e` CONDITION FOR SQLSTATE '23000';DECLARE EXIT HANDLER FOR `e` ROLLBACK; END --- case -CREATE PROCEDURE `p`() BEGIN DECLARE `e` CONDITION FOR 1051; END --- case -CREATE PROCEDURE `p`() LOOP SET @`x`=1;END LOOP --- case -CREATE PROCEDURE `p`() BEGIN LOOP SET @`x`=1;END LOOP; END --- case -CREATE PROCEDURE `p`() `lbl`: LOOP LEAVE `lbl`;END LOOP `lbl` 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 38413b9..0000000 --- a/parser/testdata/parser/mysql_unsupported_ddl/input.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE t (a INT) AUTOEXTEND_SIZE = 4194304 --- case -CREATE TABLE t (a INT) START TRANSACTION --- case -CREATE INDEX i1 ON t (a) ENGINE_ATTRIBUTE = '{}' --- case -ALTER DATABASE d READ ONLY = 1 --- case -ALTER DATABASE d READ ONLY = DEFAULT --- case -CREATE VIEW v AS SELECT 1 WITH CHECK OPTION --- case -ALTER VIEW v AS SELECT 1 WITH CHECK OPTION 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 66927a9..0000000 --- a/parser/testdata/parser/mysql_unsupported_ddl/output.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE `t` (`a` INT) AUTOEXTEND_SIZE = 4194304 --- case -CREATE TABLE `t` (`a` INT) START TRANSACTION --- case -CREATE INDEX `i1` ON `t` (`a`) ENGINE_ATTRIBUTE = '{}' --- case -ALTER DATABASE `d` READ ONLY = 1 --- case -ALTER DATABASE `d` READ ONLY = DEFAULT --- case -CREATE ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 --- case -ALTER ALGORITHM = UNDEFINED DEFINER = CURRENT_USER SQL SECURITY DEFINER VIEW `v` AS SELECT 1 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 a4b1811..0000000 --- a/parser/testdata/parser/mysql_unsupported_dml/input.sql +++ /dev/null @@ -1,15 +0,0 @@ -SELECT a INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FROM t --- case -SELECT a FROM t INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' --- case -SELECT * FROM t1, t2 FOR SHARE OF t1 NOWAIT FOR UPDATE OF t2 SKIP LOCKED --- case -SELECT * FROM t INTO @a FOR UPDATE --- case -SELECT * FROM (VALUES ROW(1, 2), ROW(3, 4)) AS v (c1, c2) --- case -LOAD DATA CONCURRENT INFILE '/tmp/f' IGNORE INTO TABLE t --- case -LOAD DATA INFILE '/tmp/f' INTO TABLE t PARTITION (p0) CHARACTER SET utf8mb4 --- case -LOAD DATA INFILE '/tmp/f' INTO TABLE t COLUMNS TERMINATED BY ',' IGNORE 2 ROWS 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 c082bdc..0000000 --- a/parser/testdata/parser/mysql_unsupported_dml/output.sql +++ /dev/null @@ -1,15 +0,0 @@ -SELECT `a` FROM `t` INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 --- case -SELECT `a` FROM `t` INTO OUTFILE '/tmp/f' CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' --- case -SELECT * FROM (`t1`) JOIN `t2` FOR SHARE OF `t1` NOWAIT FOR UPDATE OF `t2` SKIP LOCKED --- case -SELECT * FROM `t` FOR UPDATE INTO @`a` --- case -SELECT * FROM (VALUES ROW(1,2), ROW(3,4)) AS `v`(`c1`, `c2`) --- case -LOAD DATA CONCURRENT INFILE '/tmp/f' IGNORE INTO TABLE `t` --- case -LOAD DATA INFILE '/tmp/f' INTO TABLE `t` PARTITION (`p0`) CHARACTER SET utf8mb4 --- case -LOAD DATA INFILE '/tmp/f' INTO TABLE `t` FIELDS TERMINATED BY ',' IGNORE 2 LINES 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 3f82f0a..0000000 --- a/parser/testdata/parser/mysql_unsupported_replication/input.sql +++ /dev/null @@ -1,21 +0,0 @@ -CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = (1, 2) --- case -CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = () --- case -CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = 'u'@'h' --- case -CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = NULL --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = ON --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = OFF --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE --- case -CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = OFF --- case -CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = LOCAL --- case -RESET BINARY LOGS AND GTIDS TO 100 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 7db9134..0000000 --- a/parser/testdata/parser/mysql_unsupported_replication/output.sql +++ /dev/null @@ -1,21 +0,0 @@ -CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = (1, 2) --- case -CHANGE REPLICATION SOURCE TO IGNORE_SERVER_IDS = () --- case -CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = `u`@`h` --- case -CHANGE REPLICATION SOURCE TO PRIVILEGE_CHECKS_USER = NULL --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = ON --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = OFF --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = STREAM --- case -CHANGE REPLICATION SOURCE TO REQUIRE_TABLE_PRIMARY_KEY_CHECK = GENERATE --- case -CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = OFF --- case -CHANGE REPLICATION SOURCE TO ASSIGN_GTIDS_TO_ANONYMOUS_TRANSACTIONS = LOCAL --- case -RESET BINARY LOGS AND GTIDS TO 100 diff --git a/parser/testdata/parser/mysql_unsupported_show/input.sql b/parser/testdata/parser/mysql_unsupported_show/input.sql deleted file mode 100644 index 89ebc3d..0000000 --- a/parser/testdata/parser/mysql_unsupported_show/input.sql +++ /dev/null @@ -1,13 +0,0 @@ -SHOW STORAGE ENGINES --- case -SHOW ERRORS LIMIT 5 --- case -SHOW ERRORS LIMIT 5, 10 --- case -SHOW WARNINGS LIMIT 1 --- case -SHOW WARNINGS LIMIT 5, 10 --- case -SHOW EXTENDED INDEX FROM t --- case -SHOW REPLICA STATUS FOR CHANNEL 'ch' diff --git a/parser/testdata/parser/mysql_unsupported_show/output.sql b/parser/testdata/parser/mysql_unsupported_show/output.sql deleted file mode 100644 index 73939dd..0000000 --- a/parser/testdata/parser/mysql_unsupported_show/output.sql +++ /dev/null @@ -1,13 +0,0 @@ -SHOW ENGINES --- case -SHOW ERRORS LIMIT 5 --- case -SHOW ERRORS LIMIT 5,10 --- case -SHOW WARNINGS LIMIT 1 --- case -SHOW WARNINGS LIMIT 5,10 --- case -SHOW EXTENDED INDEX IN `t` --- case -SHOW REPLICA STATUS FOR CHANNEL 'ch' 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 37ff4f7..0000000 --- a/parser/testdata/parser/mysql_unsupported_txn/input.sql +++ /dev/null @@ -1,15 +0,0 @@ -START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY --- case -BEGIN WORK --- case -COMMIT WORK --- case -ROLLBACK WORK AND NO CHAIN RELEASE --- case -ROLLBACK WORK TO s1 --- case -LOCK TABLES t1 AS a1 WRITE --- case -LOCK TABLES t1 a1 READ --- case -LOCK TABLES t2 LOW_PRIORITY WRITE 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 8e20b32..0000000 --- a/parser/testdata/parser/mysql_unsupported_txn/output.sql +++ /dev/null @@ -1,15 +0,0 @@ -START TRANSACTION READ ONLY --- case -START TRANSACTION --- case -COMMIT --- case -ROLLBACK RELEASE --- case -ROLLBACK TO s1 --- case -LOCK TABLES `t1` AS `a1` WRITE --- case -LOCK TABLES `t1` AS `a1` READ --- case -LOCK TABLES `t2` LOW_PRIORITY WRITE diff --git a/parser/testdata/parser/mysql_unsupported_utility/input.sql b/parser/testdata/parser/mysql_utility/input.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_utility/input.sql rename to parser/testdata/parser/mysql_utility/input.sql diff --git a/parser/testdata/parser/mysql_unsupported_utility/output.sql b/parser/testdata/parser/mysql_utility/output.sql similarity index 100% rename from parser/testdata/parser/mysql_unsupported_utility/output.sql rename to parser/testdata/parser/mysql_utility/output.sql