From 243601d410b01fbf88f0f7e9edb55c910882ce8f Mon Sep 17 00:00:00 2001 From: Vincent Huang Date: Thu, 18 Jun 2026 22:41:25 -0700 Subject: [PATCH 1/4] feat(mariadb): parse UUID / INET4 / INET6 scalar types MariaDB-only non-reserved scalar types, recognised in type position only (UUID is also a function; INET4/INET6 usable as identifiers) so the words are not reserved. Closes part of the BYT-9135 add-surface; flows to bytebase MariaDB Diagnose on a dep bump (verified end-to-end: CREATE TABLE t (a UUID) RED->GREEN). --- mariadb/parser/type.go | 20 ++++++++++++++++ mariadb/parser/type_inet_uuid_test.go | 33 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 mariadb/parser/type_inet_uuid_test.go diff --git a/mariadb/parser/type.go b/mariadb/parser/type.go index 1950ef9a..906ba660 100644 --- a/mariadb/parser/type.go +++ b/mariadb/parser/type.go @@ -1,6 +1,8 @@ package parser import ( + "strings" + nodes "github.com/bytebase/omni/mariadb/ast" ) @@ -385,6 +387,24 @@ func (p *Parser) parseDataType() (*nodes.DataType, error) { } default: + // MariaDB-only scalar types that are non-reserved (UUID is also a function + // name, INET4/INET6 can be identifiers), so they are recognised here in type + // position only, by their identifier text, without reserving the words. + if p.cur.Type == tokIDENT { + switch { + case strings.EqualFold(p.cur.Str, "UUID"): + dt.Name = "UUID" + case strings.EqualFold(p.cur.Str, "INET4"): + dt.Name = "INET4" + case strings.EqualFold(p.cur.Str, "INET6"): + dt.Name = "INET6" + } + if dt.Name != "" { + p.advance() + dt.Loc.End = p.pos() + return dt, nil + } + } if p.cur.Type == tokEOF { return nil, p.syntaxErrorAtCur() } diff --git a/mariadb/parser/type_inet_uuid_test.go b/mariadb/parser/type_inet_uuid_test.go new file mode 100644 index 00000000..9c131398 --- /dev/null +++ b/mariadb/parser/type_inet_uuid_test.go @@ -0,0 +1,33 @@ +package parser + +import "testing" + +// TestUUIDInetTypeAccept covers the MariaDB-only scalar types UUID, INET4 and +// INET6 (BYT-9135). They are non-reserved — UUID is also a function, INET4/INET6 +// are usable as identifiers — so they are recognised only in type position. +func TestUUIDInetTypeAccept(t *testing.T) { + accept := []string{ + "CREATE TABLE t (a UUID)", + "CREATE TABLE t (a uuid)", // case-insensitive + "CREATE TABLE t (a INET4, b INET6)", + "CREATE TABLE t (id INT PRIMARY KEY, addr INET6 NOT NULL DEFAULT '::1')", + "CREATE TABLE t (u UUID DEFAULT UUID())", + "ALTER TABLE t ADD COLUMN addr INET4", + } + for _, sql := range accept { + t.Run(sql, func(t *testing.T) { ParseAndCheck(t, sql) }) + } +} + +// TestUUIDInetNonReserved pins that the words stay usable as function names and +// identifiers (the contextual recognition must not reserve them). +func TestUUIDInetNonReserved(t *testing.T) { + accept := []string{ + "SELECT UUID()", + "SELECT uuid, inet4, inet6 FROM t", + "CREATE TABLE t (uuid INT, inet4 VARCHAR(10), inet6 TEXT)", + } + for _, sql := range accept { + t.Run(sql, func(t *testing.T) { ParseAndCheck(t, sql) }) + } +} From 2fb9297f0a44b0294c5ba7746c818a0c7a2ccc67 Mon Sep 17 00:00:00 2001 From: Vincent Huang Date: Thu, 18 Jun 2026 23:46:51 -0700 Subject: [PATCH 2/4] fix(mariadb): reject FOR UPDATE/SHARE ... OF (removed in MariaDB 11.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OF object list on a locking clause is MySQL-only; MariaDB 11.4 removed it. omni mirrored MySQL and over-accepted it — a parser-fidelity bug. Gate the OF arm to a reject; FOR UPDATE / FOR SHARE (no OF) are unaffected. Also records a fidelity note in expr.go for the sibling JSON -> / ->> over-accept, deferred because its grammar one-liner ripples into inherited accept-tests + the routine-body audit (a multi-file cleanup, out of this surgical arm). --- mariadb/parser/expr.go | 8 +++++++- mariadb/parser/select.go | 16 ++++------------ mariadb/parser/subtractive_prune_test.go | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 mariadb/parser/subtractive_prune_test.go diff --git a/mariadb/parser/expr.go b/mariadb/parser/expr.go index 09e3bc39..20e7b632 100644 --- a/mariadb/parser/expr.go +++ b/mariadb/parser/expr.go @@ -266,7 +266,13 @@ func (p *Parser) infixPrecedence() (int, nodes.BinaryOp, bool) { case kwCOLLATE: return precCollate, 0, true - // JSON column-path operators + // JSON column-path operators. + // FIDELITY NOTE (BYT-9135): MariaDB has no -> / ->> operators (MySQL-only; it + // uses JSON_EXTRACT()/JSON_UNQUOTE()), so omni over-accepts them here. The + // grammar fix is one line (drop these two cases), but it ripples into the + // inherited mysql accept-tests (compare_test.go TestParseJsonExtract / + // TestParseJsonUnquoteExtract) and the routine_body_audit "-> / ->>" entries — + // a multi-file cleanup deferred from the FOR-UPDATE-OF prune. Tracked over-accept. case tokJsonExtract: return precJsonAccess, nodes.BinOpJsonExtract, true case tokJsonUnquote: diff --git a/mariadb/parser/select.go b/mariadb/parser/select.go index fd94bad4..a920eea3 100644 --- a/mariadb/parser/select.go +++ b/mariadb/parser/select.go @@ -1553,19 +1553,11 @@ func (p *Parser) parseForUpdateClause() (*nodes.ForUpdate, error) { return nil, &ParseError{Message: "collecting"} } - // OF table_list + // MariaDB removed FOR UPDATE/SHARE ... OF in 11.4 (it is MySQL-only). if p.cur.Type == kwOF { - p.advance() - for { - ref, err := p.parseTableRef() - if err != nil { - return nil, err - } - fu.Tables = append(fu.Tables, ref) - if p.cur.Type != ',' { - break - } - p.advance() + return nil, &ParseError{ + Message: "FOR UPDATE/SHARE ... OF is not supported in MariaDB", + Position: p.cur.Loc, } } diff --git a/mariadb/parser/subtractive_prune_test.go b/mariadb/parser/subtractive_prune_test.go new file mode 100644 index 00000000..a899e3d9 --- /dev/null +++ b/mariadb/parser/subtractive_prune_test.go @@ -0,0 +1,23 @@ +package parser + +import "testing" + +// TestForUpdateOfReject pins that omni/mariadb rejects FOR UPDATE/SHARE ... OF, +// which MariaDB removed in 11.4 (the OF object list is MySQL-only). FOR UPDATE / +// FOR SHARE without OF are unaffected (FOR SHARE itself remains a separately +// tracked over-accept — see subtractiveDivergences). +// +// The JSON -> / ->> arrow over-accept is a deferred sibling prune: the grammar +// fix is one line but it ripples into inherited mysql accept-tests and the +// routine_body_audit, so it is deferred from this surgical arm (see the fidelity +// note in expr.go). +func TestForUpdateOfReject(t *testing.T) { + reject := []string{ + "SELECT * FROM t FOR UPDATE OF t", + "SELECT * FROM t FOR SHARE OF t", + "SELECT * FROM t FOR UPDATE OF t, t2", + } + for _, sql := range reject { + t.Run(sql, func(t *testing.T) { ParseExpectError(t, sql) }) + } +} From 09d5b56c1aed682120c1ead5ae0de3bc3a717a91 Mon Sep 17 00:00:00 2001 From: Vincent Huang Date: Thu, 18 Jun 2026 23:51:32 -0700 Subject: [PATCH 3/4] test(mariadb): pin JSON -> / ->> over-accepts in the divergence inventory The JSON arrow prune is deferred (ripples into inherited accept-tests + the routine-body audit). Pin the two over-accepts in subtractiveDivergences + categoryCaseSQLs so the inventory gate stays honest and the finding is tracked, not floating. Container-verified OVER vs mariadb:11.8.8 (TestMariaDBDivergenceInventory green). --- mariadb/parser/oracle_corpus_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mariadb/parser/oracle_corpus_test.go b/mariadb/parser/oracle_corpus_test.go index d5274dc6..e36a3254 100644 --- a/mariadb/parser/oracle_corpus_test.go +++ b/mariadb/parser/oracle_corpus_test.go @@ -205,6 +205,8 @@ var categoryCaseSQLs = []string{ "SELECT 1 MINUS SELECT 2", "SELECT 1 MINUS SELECT 2 MINUS SELECT 3", "SELECT * FROM t WHERE id = 1 FOR SHARE", + "SELECT name -> '$.b' FROM t", + "SELECT name ->> '$.b' FROM t", } // containerFatalVerbs name statements whose *execution* would kill the shared @@ -289,6 +291,16 @@ var subtractiveDivergences = []divergence{ omniSide: true, // omni mirrors MySQL's FOR SHARE; MariaDB rejects it (1064). reason: "FOR SHARE locking clause: valid in MySQL 8.0 and omni mirrors it; MariaDB has no FOR SHARE (uses LOCK IN SHARE MODE) and rejects at parse (1064). Phase-0 mdbcheck OVER finding.", }, + { + sql: "SELECT name -> '$.b' FROM t", + omniSide: true, // omni mirrors MySQL's -> JSON operator; MariaDB rejects it (1064). + reason: "JSON -> column-path operator: MySQL-only; omni mirrors MySQL and over-accepts. MariaDB has no -> / ->> operators (uses JSON_EXTRACT()/JSON_UNQUOTE()) and rejects at parse (1064). Deferred prune: the one-line grammar fix (drop the tokJsonExtract/tokJsonUnquote arms in expr.go) ripples into inherited mysql accept-tests (TestParseJsonExtract/UnquoteExtract) and the routine_body_audit, beyond the surgical FOR-UPDATE-OF arm.", + }, + { + sql: "SELECT name ->> '$.b' FROM t", + omniSide: true, // ->> shares the -> divergence. + reason: "JSON ->> column-path unquote operator: MySQL-only; same divergence as -> (omni over-accepts, MariaDB rejects 1064). Deferred with the -> prune.", + }, } func TestMariaDBDivergenceInventory(t *testing.T) { From aa25a7316623cf4b717028f4d4603d0c1ca82d17 Mon Sep 17 00:00:00 2001 From: Vincent Huang Date: Fri, 19 Jun 2026 00:01:34 -0700 Subject: [PATCH 4/4] fix(mariadb): drop FOR UPDATE/SHARE OF from deparse to match the parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parser now rejects FOR UPDATE/SHARE ... OF (MariaDB 11.4 removal), so the deparse round-trip fixture for it no longer parses (deparse_test.go). Remove the fixture and the now-dead OF-emission in deparseForUpdate, so parse and deparse stay consistent — neither supports FOR UPDATE OF. --- mariadb/deparse/deparse.go | 14 -------------- mariadb/deparse/deparse_test.go | 5 ----- 2 files changed, 19 deletions(-) diff --git a/mariadb/deparse/deparse.go b/mariadb/deparse/deparse.go index 17385f82..76ca74ee 100644 --- a/mariadb/deparse/deparse.go +++ b/mariadb/deparse/deparse.go @@ -180,7 +180,6 @@ func deparseSelectStmtCtx(stmt *ast.SelectStmt, suppressAlias bool) string { // - for update // - for share // - lock in share mode (legacy syntax) -// - for update of `t` // - for update nowait // - for update skip locked func deparseForUpdate(fu *ast.ForUpdate) string { @@ -195,19 +194,6 @@ func deparseForUpdate(fu *ast.ForUpdate) string { b.WriteString("for update") } - // OF table list - if len(fu.Tables) > 0 { - b.WriteString(" of ") - for i, tbl := range fu.Tables { - if i > 0 { - b.WriteString(",") - } - b.WriteString("`") - b.WriteString(tbl.Name) - b.WriteString("`") - } - } - // NOWAIT / SKIP LOCKED if fu.NoWait { b.WriteString(" nowait") diff --git a/mariadb/deparse/deparse_test.go b/mariadb/deparse/deparse_test.go index 7eb962ec..a6bd4ea4 100644 --- a/mariadb/deparse/deparse_test.go +++ b/mariadb/deparse/deparse_test.go @@ -1339,11 +1339,6 @@ func TestDeparse_Section_5_8_ForUpdate(t *testing.T) { "SELECT a FROM t LOCK IN SHARE MODE", "select `a` AS `a` from `t` lock in share mode", }, - { - "for_update_of_table", - "SELECT a FROM t FOR UPDATE OF t", - "select `a` AS `a` from `t` for update of `t`", - }, { "for_update_nowait", "SELECT a FROM t FOR UPDATE NOWAIT",