- 实现了标识符处理器,统一处理表名、字段名的前缀添加和引号转换 - 添加对 MySQL、PostgreSQL、SQLite 三种数据库方言的支持 - 引入 ProcessTableName、ProcessColumn、ProcessConditionString 等方法处理标识符 - 为 HoTimeDB 添加 T() 和 C() 辅助方法用于手动构建 SQL 查询 - 重构 CRUD 操作中的表名和字段名处理逻辑,统一使用标识符处理器 - 添加完整的单元测试验证不同数据库方言下的标识符处理功能 - 优化 JOIN 操作中表名和条件字符串的处理方式
240 lines
7.5 KiB
Go
240 lines
7.5 KiB
Go
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,
|
||
}
|
||
}
|
||
|
||
// ProcessTableName 处理表名(添加前缀+引号)
|
||
// 输入: "order" 或 "`order`" 或 "\"order\""
|
||
// 输出: "`app_order`" (MySQL) 或 "\"app_order\"" (PostgreSQL/SQLite)
|
||
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.dialect.QuoteIdentifier(p.prefix+tableName) + " " + alias
|
||
}
|
||
|
||
// 添加前缀和引号
|
||
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`" (MySQL)
|
||
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])
|
||
// 表名添加前缀
|
||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(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))
|
||
}
|
||
|
||
parts := p.splitTableColumn(name)
|
||
if len(parts) == 2 {
|
||
tableName := p.stripQuotes(parts[0])
|
||
columnName := p.stripQuotes(parts[1])
|
||
return p.dialect.QuoteIdentifier(tableName) + "." + p.dialect.QuoteIdentifier(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" (MySQL)
|
||
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]
|
||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(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)
|
||
}
|
||
}
|
||
return p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(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] // 后面的边界字符
|
||
return prefix + p.dialect.QuoteIdentifier(p.prefix+tableName) + "." + p.dialect.QuoteIdentifier(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
|
||
}
|