Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 56 additions & 8 deletions internal/impl/mysql/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package mysql

import (
"errors"
"regexp"
"unicode/utf8"
)

Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions internal/impl/mysql/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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 {
Expand Down
Loading