From d4ef97b009351b1e2333f77d0af682b9868c60b3 Mon Sep 17 00:00:00 2001 From: Sam Agarwal Date: Mon, 31 Aug 2026 14:14:15 -0400 Subject: [PATCH] fix(mysql_cdc): accept unicode table names MySQL permits two sets of characters in unquoted identifiers: the ASCII set [0-9a-zA-Z$_] and the extended range U+0080 to U+FFFF. validateTableName only implemented the ASCII half, so a table named cafe with an accent, or one named in Japanese or Cyrillic, was rejected before the stream started and could not be captured at all. Replace the two regexes with explicit rune checks that also accept the extended range, and reject supplementary characters (U+10000 and above) and malformed UTF-8, neither of which MySQL permits. --- internal/impl/mysql/validate.go | 64 ++++++++++++++++++++++++---- internal/impl/mysql/validate_test.go | 38 +++++++++++++++++ 2 files changed, 94 insertions(+), 8 deletions(-) diff --git a/internal/impl/mysql/validate.go b/internal/impl/mysql/validate.go index 6c4ae184e8..e52c09bb40 100644 --- a/internal/impl/mysql/validate.go +++ b/internal/impl/mysql/validate.go @@ -10,7 +10,6 @@ package mysql import ( "errors" - "regexp" "unicode/utf8" ) @@ -21,25 +20,74 @@ var ( errInvalidTableName = errors.New("invalid table name") ) +// isExtendedIdentifierRune reports whether r falls in the extended range MySQL +// permits in unquoted identifiers, U+0080 to U+FFFF. Supplementary characters +// (U+10000 and above) are not permitted. +// +// See https://dev.mysql.com/doc/refman/8.4/en/identifiers.html +func isExtendedIdentifierRune(r rune) bool { + return r >= 0x80 && r <= 0xFFFF +} + +// isIdentifierStartRune reports whether r may begin an unquoted table name. +func isIdentifierStartRune(r rune) bool { + switch { + case r == '_': + return true + case 'a' <= r && r <= 'z': + return true + case 'A' <= r && r <= 'Z': + return true + default: + return isExtendedIdentifierRune(r) + } +} + +// isIdentifierRune reports whether r may appear after the first character of an +// unquoted table name. +func isIdentifierRune(r rune) bool { + switch { + case '0' <= r && r <= '9': + return true + case r == '$': + return true + default: + return isIdentifierStartRune(r) + } +} + func validateTableName(tableName string) error { // Check if empty if tableName == "" { return errEmptyTableName } + // Reject malformed input up front. Ranging over a string yields + // utf8.RuneError for an invalid byte, which sits inside the extended range + // and would otherwise be accepted. + if !utf8.ValidString(tableName) { + return errInvalidTableName + } + // Check length if utf8.RuneCountInString(tableName) > 64 { return errInvalidTableLength } - // Check if starts with a valid character - if matched, _ := regexp.MatchString(`^[a-zA-Z_]`, tableName); !matched { - return errInvalidTableStartChar - } + for i, r := range tableName { + // i is a byte offset, so it is only zero for the first rune. + if i == 0 { + // Check if starts with a valid character + if !isIdentifierStartRune(r) { + return errInvalidTableStartChar + } + continue + } - // Check if contains only valid characters - if matched, _ := regexp.MatchString(`^[a-zA-Z0-9_$]+$`, tableName); !matched { - return errInvalidTableName + // Check if contains only valid characters + if !isIdentifierRune(r) { + return errInvalidTableName + } } return nil diff --git a/internal/impl/mysql/validate_test.go b/internal/impl/mysql/validate_test.go index 0dd304ce18..9beee94512 100644 --- a/internal/impl/mysql/validate_test.go +++ b/internal/impl/mysql/validate_test.go @@ -45,6 +45,29 @@ func TestValidateTableName(t *testing.T) { tableName: "UserProfiles", expectedErr: nil, }, + // MySQL permits the extended range U+0080..U+FFFF in unquoted + // identifiers, alongside the ASCII set. + // https://dev.mysql.com/doc/refman/8.4/en/identifiers.html + { + name: "Valid table name with accented latin characters", + tableName: "café", + expectedErr: nil, + }, + { + name: "Valid table name starting with an extended character", + tableName: "日本語テーブル", + expectedErr: nil, + }, + { + name: "Valid table name with cyrillic characters", + tableName: "Пользователи", + expectedErr: nil, + }, + { + name: "Valid table name mixing ascii and extended characters", + tableName: "orders_ñ_2024", + expectedErr: nil, + }, // Invalid cases { @@ -77,6 +100,21 @@ func TestValidateTableName(t *testing.T) { tableName: strings.Repeat("a", 65), expectedErr: errInvalidTableLength, }, + { + name: "Too long table name counted in runes", + tableName: strings.Repeat("é", 65), + expectedErr: errInvalidTableLength, + }, + { + name: "Supplementary characters are not permitted", + tableName: "table_😀", + expectedErr: errInvalidTableName, + }, + { + name: "Invalid utf-8 is rejected", + tableName: "table_" + string([]byte{0xff}), + expectedErr: errInvalidTableName, + }, } for _, tc := range tests {