Files
relspecgo/pkg/readers/mysql/defaults.go
T
warkanum 1d93255575 fix(assets): improve error reporting for asset failures
fix(embed): update embedDirectives to return result directly

fix(executor): clean up import order and formatting

fix(syntax_test): change file permission syntax to octal

fix(mysql): update file permission syntax to octal

fix(pgsql): update file permission syntax to octal

fix(sqlex): simplify Walk function parameter handling

fix(transform): adjust identifier comparison logic

fix(template): clean up function formatting and error handling
2026-10-12 22:04:40 +02:00

195 lines
4.2 KiB
Go

package mysql
import (
"regexp"
"strings"
)
// columnDefaults extracts native DEFAULT expressions from SHOW CREATE TABLE.
// MySQL can truncate binary defaults in information_schema (for example X'00FF'
// appears as just "0x"). SHOW CREATE is authoritative for their SQL spelling.
// Separators and whitespace inside quoted strings, identifiers and nested
// expressions are never treated as definition/default boundaries.
func columnDefaults(create string) map[string]string {
out := map[string]string{}
depth, start := 0, 0
var quoted byte
definitions := []string{}
for i := 0; i < len(create); i++ {
ch := create[i]
if quoted != 0 {
if ch == '\\' && quoted != '`' && i+1 < len(create) {
i++
continue
}
if ch == quoted {
if i+1 < len(create) && create[i+1] == quoted {
i++
} else {
quoted = 0
}
}
continue
}
if ch == '\'' || ch == '"' || ch == '`' {
quoted = ch
continue
}
switch ch {
case '(':
depth++
if depth == 1 {
start = i + 1
}
case ',':
if depth == 1 {
definitions = append(definitions, create[start:i])
start = i + 1
}
case ')':
depth--
if depth == 0 {
definitions = append(definitions, create[start:i])
}
}
if ch == ')' && depth == 0 {
break
}
}
for _, def := range definitions {
def = strings.TrimSpace(def)
if len(def) == 0 || def[0] != '`' {
continue
}
var name strings.Builder
end := 1
for end < len(def) {
if def[end] == '`' {
if end+1 < len(def) && def[end+1] == '`' {
name.WriteByte('`')
end += 2
continue
}
end++
break
}
name.WriteByte(def[end])
end++
}
// Consume full tokens so DEFAULT inside a generated expression or literal
// cannot masquerade as a column default.
for end < len(def) {
for end < len(def) && isSpace(def[end]) {
end++
}
begin := end
end = tokenEnd(def, end)
if end == begin {
break
}
if strings.EqualFold(def[begin:end], "DEFAULT") {
for end < len(def) && isSpace(def[end]) {
end++
}
finish := tokenEnd(def, end)
out[name.String()] = def[end:finish]
break
}
}
}
return out
}
func isSpace(b byte) bool { return b == ' ' || b == '\n' || b == '\r' || b == '\t' }
func tokenEnd(s string, start int) int {
depth := 0
var quoted byte
for i := start; i < len(s); i++ {
ch := s[i]
if quoted != 0 {
if ch == '\\' && quoted != '`' && i+1 < len(s) {
i++
continue
}
if ch == quoted {
if i+1 < len(s) && s[i+1] == quoted {
i++
} else {
quoted = 0
}
}
continue
}
if ch == '\'' || ch == '"' || ch == '`' {
quoted = ch
continue
}
if ch == '(' {
depth++
}
if ch == ')' {
depth--
}
if depth == 0 && isSpace(ch) {
return i
}
}
return len(s)
}
// SHOW CREATE has native options not exposed by STATISTICS/COLUMNS. Ignore
// quoted identifiers and values when detecting options outside our contract.
func unquotedSQL(create string) string {
var code strings.Builder
var quoted byte
for i := 0; i < len(create); i++ {
ch := create[i]
if quoted != 0 {
if ch == '\\' && quoted != '`' && i+1 < len(create) {
i++
continue
}
if ch == quoted {
if i+1 < len(create) && create[i+1] == quoted {
i++
} else {
quoted = 0
code.WriteByte(' ')
}
}
continue
}
if ch == '\'' || ch == '"' || ch == '`' {
quoted = ch
code.WriteByte(' ')
continue
}
code.WriteByte(ch)
}
return code.String()
}
var autoIncrementOption = regexp.MustCompile(`(?i)\bAUTO_INCREMENT\s*=\s*([0-9]+)`)
func autoIncrementValue(create string) string {
if match := autoIncrementOption.FindStringSubmatch(unquotedSQL(create)); len(match) > 1 {
return match[1]
}
return "1"
}
func unsupportedNativeOptions(create string) []string {
words := strings.FieldsFunc(strings.ToUpper(unquotedSQL(create)), func(r rune) bool { return (r < 'A' || r > 'Z') && r != '_' })
var out []string
for i, w := range words {
switch w {
case "ENGINE_ATTRIBUTE", "SECONDARY_ENGINE_ATTRIBUTE", "KEY_BLOCK_SIZE", "COLUMN_FORMAT", "STORAGE":
out = append(out, w)
case "PARSER":
if i > 0 && words[i-1] == "WITH" {
out = append(out, "WITH PARSER")
}
}
}
return out
}