hotime/db/identifier.go
hoteas c922023740 fix(db): 修复表列标识符处理逻辑确保聚合函数正常工作
- 修改 ProcessColumn 方法,table.column 格式不再添加反引号,只添加前缀
- 修改 ProcessColumnNoPrefix 方法,table.column 格式不加引号保持原始格式
- 更新 ProcessConditionString 方法,条件字符串中的 table.column 不加反引号
- 修复聚合函数如 Sum、Avg、Max、Min 在 table.column 格式下的正确性
- 添加完整的 table.column 格式测试用例验证功能一致性
- 确保返回的列名与原始列名一致,便于聚合函数等场景读取结果
2026-02-02 11:02:59 +08:00

276 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package db
import (
"regexp"
"strings"
)
// IdentifierProcessor 标识符处理器
// 用于处理表名、字段名的前缀添加和引号转换
type IdentifierProcessor struct {
dialect Dialect
prefix string
}
// NewIdentifierProcessor 创建标识符处理器
func NewIdentifierProcessor(dialect Dialect, prefix string) *IdentifierProcessor {
return &IdentifierProcessor{
dialect: dialect,
prefix: prefix,
}
}
// 系统数据库列表,这些数据库不添加前缀
var systemDatabases = map[string]bool{
"INFORMATION_SCHEMA": true,
"information_schema": true,
"mysql": true,
"performance_schema": true,
"sys": true,
"pg_catalog": true,
"pg_toast": true,
}
// ProcessTableName 处理表名(添加前缀+引号)
// 输入: "order" 或 "`order`" 或 "\"order\"" 或 "INFORMATION_SCHEMA.TABLES"
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
// 对于 database.table 格式,会分别处理,系统数据库不添加前缀
func (p *IdentifierProcessor) ProcessTableName(name string) string {
// 去除已有的引号
name = p.stripQuotes(name)
// 检查是否包含空格(别名情况,如 "order AS o"
if strings.Contains(name, " ") {
// 处理别名情况
parts := strings.SplitN(name, " ", 2)
tableName := p.stripQuotes(parts[0])
alias := parts[1]
// 递归处理表名部分(可能包含点号)
return p.ProcessTableName(tableName) + " " + alias
}
// 检查是否包含点号database.table 格式)
if strings.Contains(name, ".") {
parts := p.splitTableColumn(name)
if len(parts) == 2 {
dbName := p.stripQuotes(parts[0])
tableName := p.stripQuotes(parts[1])
// 系统数据库不添加前缀
if systemDatabases[dbName] {
return p.dialect.QuoteIdentifier(dbName) + "." + p.dialect.QuoteIdentifier(tableName)
}
// 非系统数据库,只给表名添加前缀
return p.dialect.QuoteIdentifier(dbName) + "." + p.dialect.QuoteIdentifier(p.prefix+tableName)
}
}
// 添加前缀和引号
return p.dialect.QuoteIdentifier(p.prefix + name)
}
// ProcessTableNameNoPrefix 处理表名(只添加引号,不添加前缀)
// 用于已经包含前缀的情况
func (p *IdentifierProcessor) ProcessTableNameNoPrefix(name string) string {
name = p.stripQuotes(name)
if strings.Contains(name, " ") {
parts := strings.SplitN(name, " ", 2)
tableName := p.stripQuotes(parts[0])
alias := parts[1]
return p.dialect.QuoteIdentifier(tableName) + " " + alias
}
return p.dialect.QuoteIdentifier(name)
}
// ProcessColumn 处理 table.column 格式
// 输入: "name" 或 "order.name" 或 "`order`.name" 或 "`order`.`name`"
// 输出: "`name`" 或 "app_order.name"
// 注意: 单独的列名加引号避免关键字冲突table.column 格式不加引号
func (p *IdentifierProcessor) ProcessColumn(name string) string {
// 检查是否包含点号
if !strings.Contains(name, ".") {
// 单独的列名,需要加引号(避免关键字冲突)
return p.dialect.QuoteIdentifier(p.stripQuotes(name))
}
// 处理 table.column 格式,不加引号,只添加前缀
parts := p.splitTableColumn(name)
if len(parts) == 2 {
tableName := p.stripQuotes(parts[0])
columnName := p.stripQuotes(parts[1])
// table.column 格式不加反引号
return p.prefix + tableName + "." + columnName
}
// 无法解析,返回原样但转换引号
return p.convertQuotes(name)
}
// ProcessColumnNoPrefix 处理 table.column 格式(不添加前缀)
func (p *IdentifierProcessor) ProcessColumnNoPrefix(name string) string {
if !strings.Contains(name, ".") {
// 单独的列名,需要加引号(避免关键字冲突)
return p.dialect.QuoteIdentifier(p.stripQuotes(name))
}
// table.column 格式不加引号
parts := p.splitTableColumn(name)
if len(parts) == 2 {
tableName := p.stripQuotes(parts[0])
columnName := p.stripQuotes(parts[1])
return tableName + "." + columnName
}
return p.convertQuotes(name)
}
// ProcessConditionString 智能解析条件字符串(如 ON 条件)
// 输入: "user.id = order.user_id AND order.status = 1"
// 输出: "app_user.id = app_order.user_id AND app_order.status = 1"
// 注意: table.column 格式不加反引号,因为 MySQL/SQLite/PostgreSQL 都能正确解析
// 这样可以保持返回的列名与原始列名一致,便于聚合函数等场景读取结果
func (p *IdentifierProcessor) ProcessConditionString(condition string) string {
if condition == "" {
return condition
}
result := condition
// 首先处理已有完整引号的情况 `table`.`column` 或 "table"."column"
// 去除引号,只添加前缀
fullyQuotedPattern := regexp.MustCompile("[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]\\.[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]")
result = fullyQuotedPattern.ReplaceAllStringFunc(result, func(match string) string {
parts := fullyQuotedPattern.FindStringSubmatch(match)
if len(parts) == 3 {
tableName := parts[1]
colName := parts[2]
// table.column 格式不加反引号,只添加前缀
return p.prefix + tableName + "." + colName
}
return match
})
// 然后处理部分引号的情况 `table`.column 或 "table".column
// 去除引号,只添加前缀
quotedTablePattern := regexp.MustCompile("[`\"]([a-zA-Z_][a-zA-Z0-9_]*)[`\"]\\.([a-zA-Z_][a-zA-Z0-9_]*)(?:[^`\"]|$)")
result = quotedTablePattern.ReplaceAllStringFunc(result, func(match string) string {
parts := quotedTablePattern.FindStringSubmatch(match)
if len(parts) >= 3 {
tableName := parts[1]
colName := parts[2]
// 保留末尾字符(如果有)
suffix := ""
if len(match) > len(parts[0])-1 {
lastChar := match[len(match)-1]
if lastChar != '`' && lastChar != '"' && !isIdentChar(lastChar) {
suffix = string(lastChar)
}
}
// table.column 格式不加反引号,只添加前缀
return p.prefix + tableName + "." + colName + suffix
}
return match
})
// 最后处理无引号的情况 table.column
// 使用更精确的正则,确保不匹配已处理的内容
unquotedPattern := regexp.MustCompile(`([^` + "`" + `"\w]|^)([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)([^` + "`" + `"\w(]|$)`)
result = unquotedPattern.ReplaceAllStringFunc(result, func(match string) string {
parts := unquotedPattern.FindStringSubmatch(match)
if len(parts) >= 5 {
prefix := parts[1] // 前面的边界字符
tableName := parts[2]
colName := parts[3]
suffix := parts[4] // 后面的边界字符
// table.column 格式不加反引号,只添加前缀
return prefix + p.prefix + tableName + "." + colName + suffix
}
return match
})
return result
}
// isIdentChar 判断是否是标识符字符
func isIdentChar(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}
// ProcessFieldList 处理字段列表字符串
// 输入: "order.id, user.name AS uname, COUNT(*)"
// 输出: "`app_order`.`id`, `app_user`.`name` AS uname, COUNT(*)" (MySQL)
func (p *IdentifierProcessor) ProcessFieldList(fields string) string {
if fields == "" || fields == "*" {
return fields
}
// 使用与 ProcessConditionString 相同的逻辑
return p.ProcessConditionString(fields)
}
// stripQuotes 去除标识符两端的引号(反引号或双引号)
func (p *IdentifierProcessor) stripQuotes(name string) string {
name = strings.TrimSpace(name)
// 去除反引号
if strings.HasPrefix(name, "`") && strings.HasSuffix(name, "`") {
return name[1 : len(name)-1]
}
// 去除双引号
if strings.HasPrefix(name, "\"") && strings.HasSuffix(name, "\"") {
return name[1 : len(name)-1]
}
return name
}
// splitTableColumn 分割 table.column 格式
// 支持: table.column, `table`.column, `table`.`column`, "table".column 等
func (p *IdentifierProcessor) splitTableColumn(name string) []string {
// 先尝试按点号分割
dotIndex := -1
// 查找不在引号内的点号
inQuote := false
quoteChar := byte(0)
for i := 0; i < len(name); i++ {
c := name[i]
if c == '`' || c == '"' {
if !inQuote {
inQuote = true
quoteChar = c
} else if c == quoteChar {
inQuote = false
}
} else if c == '.' && !inQuote {
dotIndex = i
break
}
}
if dotIndex == -1 {
return []string{name}
}
return []string{name[:dotIndex], name[dotIndex+1:]}
}
// convertQuotes 将已有的引号转换为当前方言的引号格式
func (p *IdentifierProcessor) convertQuotes(name string) string {
quoteChar := p.dialect.QuoteChar()
// 替换反引号
name = strings.ReplaceAll(name, "`", quoteChar)
// 如果目标是反引号,需要替换双引号
if quoteChar == "`" {
name = strings.ReplaceAll(name, "\"", quoteChar)
}
return name
}
// GetDialect 获取方言
func (p *IdentifierProcessor) GetDialect() Dialect {
return p.dialect
}
// GetPrefix 获取前缀
func (p *IdentifierProcessor) GetPrefix() string {
return p.prefix
}