|
| 1 | +// Package qm_namedsql 提供了对 SQL 命名参数解析为 ? 占位符 SQL 的支持(用于:MySQL、SQLite 等使用 ? 占位符的驱动)。 |
| 2 | +package qm_namedsql |
| 3 | + |
| 4 | +import ( |
| 5 | + "fmt" |
| 6 | + "reflect" |
| 7 | + "strconv" |
| 8 | + "strings" |
| 9 | + "unicode/utf8" |
| 10 | + |
| 11 | + "github.com/bunnier/sqlmer" |
| 12 | +) |
| 13 | + |
| 14 | +// 是 SQL 命名参数解析后的结果。 |
| 15 | +type ParsedResult struct { |
| 16 | + Sql string // 处理后的 SQL 语句。 |
| 17 | + Names []string // 原语句中用到的命名参数集合(没有去重逻辑,如果有同名参数,会有多项)。 |
| 18 | +} |
| 19 | + |
| 20 | +// 分析 SQL 语句,提取用到的命名参数名称(按顺序),并将 @ 占位参数转换为驱动支持的 ? 形式。 |
| 21 | +// 结果会被缓存以提升重复调用性能;无命名参数的 SQL 不写入缓存,以防止动态拼接语句导致内存无限增长。 |
| 22 | +func ParseNamedSqlToQuestionMark(sqlText string) ParsedResult { |
| 23 | + // 如果缓存中有数据,直接返回。 |
| 24 | + if cacheResult, ok := parsedSqlCache.load(sqlText); ok { |
| 25 | + return cacheResult |
| 26 | + } |
| 27 | + |
| 28 | + names := make([]string, 0, 10) // 存放 SQL 中所有的参数名称。 |
| 29 | + |
| 30 | + fixedSqlTextBuilder := strings.Builder{} |
| 31 | + paramNameBuilder := strings.Builder{} |
| 32 | + |
| 33 | + inName := false // 标示当前字符是否正处于参数名称之中。 |
| 34 | + inString := false // 标示当前字符是否正处于字符串之中。 |
| 35 | + |
| 36 | + lastIndex := utf8.RuneCountInString(sqlText) - 1 // SQL 语句的最后一个字符索引位置。 |
| 37 | + |
| 38 | + for i, currentRune := range sqlText { |
| 39 | + switch { |
| 40 | + // 遇到字符串开始/结束符号 ' 需要切换字符串状态。 |
| 41 | + case currentRune == '\'': |
| 42 | + inString = !inString |
| 43 | + fixedSqlTextBuilder.WriteRune(currentRune) |
| 44 | + |
| 45 | + // 如果当前状态正处于字符串中,字符无须特殊处理。 |
| 46 | + case inString: |
| 47 | + fixedSqlTextBuilder.WriteRune(currentRune) |
| 48 | + |
| 49 | + // @ 符号标示参数名称部分开始。 |
| 50 | + case currentRune == '@': |
| 51 | + // 连续 2 个 @ 可以用来转义,需要跳出参数名作用域。 |
| 52 | + if inName && i > 0 && sqlText[i-1] == '@' { |
| 53 | + fixedSqlTextBuilder.WriteRune(currentRune) |
| 54 | + inName = false |
| 55 | + continue |
| 56 | + } |
| 57 | + inName = true |
| 58 | + paramNameBuilder.Reset() |
| 59 | + |
| 60 | + // 当前状态正处于参数名中,且当前字符合法,应作为参数名的一部分。 |
| 61 | + case inName && isLegalParamNameCharter(currentRune): |
| 62 | + paramNameBuilder.WriteRune(currentRune) |
| 63 | + if lastIndex == i { // 如果是最后一个字符,直接写入并结束。 |
| 64 | + fixedSqlTextBuilder.WriteString("?") |
| 65 | + names = append(names, paramNameBuilder.String()) |
| 66 | + } |
| 67 | + |
| 68 | + // 当前状态正处于参数名中,且当前字符不是合法参数字符,标示着参数名范围结束。 |
| 69 | + case inName && !isLegalParamNameCharter(currentRune): |
| 70 | + inName = false |
| 71 | + fixedSqlTextBuilder.WriteString("?") |
| 72 | + fixedSqlTextBuilder.WriteRune(currentRune) |
| 73 | + names = append(names, paramNameBuilder.String()) |
| 74 | + |
| 75 | + // 非上述情况即为普通 SQL 字符部分,无须特殊处理。 |
| 76 | + default: |
| 77 | + fixedSqlTextBuilder.WriteRune(currentRune) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + parsedResult := ParsedResult{fixedSqlTextBuilder.String(), names} |
| 82 | + // 无命名参数的 SQL 通常是动态拼接的语句,跳过缓存以防止内存无限增长。 |
| 83 | + if len(names) > 0 { |
| 84 | + parsedSqlCache.store(sqlText, parsedResult) |
| 85 | + } |
| 86 | + return parsedResult |
| 87 | +} |
| 88 | + |
| 89 | +// 对使用 ? 占位符的驱动(如 MySQL、SQLite)做参数预处理。 |
| 90 | +// 若唯一参数为 map,则按 SQL 中的 @name 顺序绑定;否则按 @p1、@p2 形式从 args 切片取位置参数。 |
| 91 | +func BindQuestionMarkArgs(sqlText string, args ...any) (string, []any, error) { |
| 92 | + namedParsedResult := ParseNamedSqlToQuestionMark(sqlText) |
| 93 | + parsedNames := len(namedParsedResult.Names) |
| 94 | + argsCount := len(args) |
| 95 | + resultArgs := make([]any, 0, parsedNames) |
| 96 | + |
| 97 | + // map 按返回的 paramNames 顺序整理一个 slice 返回。 |
| 98 | + if argsCount == 1 && reflect.ValueOf(args[0]).Kind() == reflect.Map { |
| 99 | + mapArgs := args[0].(map[string]any) |
| 100 | + for _, paramName := range namedParsedResult.Names { // 这里通过从 SQL 解析出来的参数名去取参数,刚好也解决了参数重复使用的问题。 |
| 101 | + if value, ok := mapArgs[paramName]; ok { |
| 102 | + resultArgs = append(resultArgs, value) |
| 103 | + } else { |
| 104 | + return "", nil, fmt.Errorf("%w:\nlack of parameter\nsql = %s", sqlmer.ErrParseParamFailed, namedParsedResult.Sql) |
| 105 | + } |
| 106 | + } |
| 107 | + namedParsedResult.Sql, resultArgs = extendInParams(namedParsedResult.Sql, resultArgs) |
| 108 | + return namedParsedResult.Sql, resultArgs, nil |
| 109 | + } |
| 110 | + |
| 111 | + // slice 语句中使用的顺序未必是递增的,且可能重复引用同一个索引,所以这里也需要整理顺序。 |
| 112 | + for _, paramName := range namedParsedResult.Names { |
| 113 | + if paramName[0] != 'p' { // 要求数字参数的格式为 @p1....@pN。 |
| 114 | + return "", nil, fmt.Errorf("%w: parsing parameter failed\nsql = %s", sqlmer.ErrParseParamFailed, namedParsedResult.Sql) |
| 115 | + } |
| 116 | + |
| 117 | + index, err := strconv.Atoi(paramName[1:]) // 取出索引值。 |
| 118 | + if err != nil { |
| 119 | + return "", nil, fmt.Errorf("%w: parsing parameter failed\nsql = %s", sqlmer.ErrParseParamFailed, namedParsedResult.Sql) |
| 120 | + } |
| 121 | + |
| 122 | + index-- // 占位符从 1 开始,转成索引,需要 -1。 |
| 123 | + |
| 124 | + if index < 0 { // 数字参数的数值从 1 开始,而不是 0,因此不会是负数。 |
| 125 | + return "", nil, fmt.Errorf("%w: index must start at 1\nsql = %s", sqlmer.ErrParseParamFailed, namedParsedResult.Sql) |
| 126 | + } |
| 127 | + |
| 128 | + if index >= argsCount { // 索引值超过参数数量了。 |
| 129 | + return "", nil, fmt.Errorf("%w: parameter index out of args range\nsql = %s", sqlmer.ErrParseParamFailed, namedParsedResult.Sql) |
| 130 | + } |
| 131 | + |
| 132 | + resultArgs = append(resultArgs, args[index]) |
| 133 | + } |
| 134 | + |
| 135 | + namedParsedResult.Sql, resultArgs = extendInParams(namedParsedResult.Sql, resultArgs) |
| 136 | + return namedParsedResult.Sql, resultArgs, nil |
| 137 | +} |
| 138 | + |
| 139 | +// extendInParams 处理 SQL IN 子句的参数展开,将切片类型的参数展开为多个问号占位符。 |
| 140 | +func extendInParams(sqlText string, params []any) (string, []any) { |
| 141 | + newParams := make([]any, 0, len(params)) |
| 142 | + var newSqlBuilder strings.Builder |
| 143 | + |
| 144 | + paramIndex := 0 |
| 145 | + inString := false |
| 146 | + for _, r := range sqlText { |
| 147 | + if r == '\'' { |
| 148 | + inString = !inString |
| 149 | + newSqlBuilder.WriteRune(r) |
| 150 | + continue |
| 151 | + } |
| 152 | + |
| 153 | + if inString { |
| 154 | + newSqlBuilder.WriteRune(r) |
| 155 | + continue |
| 156 | + } |
| 157 | + |
| 158 | + if r != '?' { |
| 159 | + newSqlBuilder.WriteRune(r) |
| 160 | + continue |
| 161 | + } |
| 162 | + |
| 163 | + if paramIndex >= len(params) { // 后面没参数了,无需判断了。 |
| 164 | + newSqlBuilder.WriteRune(r) |
| 165 | + continue |
| 166 | + } |
| 167 | + |
| 168 | + param := params[paramIndex] |
| 169 | + paramValue := reflect.ValueOf(param) |
| 170 | + paramIndex++ |
| 171 | + |
| 172 | + // 处理切片类型。 |
| 173 | + // 排除 []byte,因为虽然 []byte 也是切片类型,但它是二进制数据,不应该被展开。 |
| 174 | + if (paramValue.Kind() == reflect.Slice || paramValue.Kind() == reflect.Array) && !paramValue.Type().ConvertibleTo(reflect.TypeOf([]byte{})) { |
| 175 | + paramLen := paramValue.Len() |
| 176 | + if paramLen == 0 { |
| 177 | + // 空切片替换为 SQL 不可能条件。 |
| 178 | + newSqlBuilder.WriteString("NULL") |
| 179 | + continue |
| 180 | + } |
| 181 | + |
| 182 | + // 生成占位符。 |
| 183 | + placeholders := strings.Repeat(",?", paramLen) |
| 184 | + newSqlBuilder.WriteString(placeholders[1:]) |
| 185 | + |
| 186 | + // 展开参数。 |
| 187 | + for i := 0; i < paramLen; i++ { |
| 188 | + newParams = append(newParams, paramValue.Index(i).Interface()) |
| 189 | + } |
| 190 | + } else { |
| 191 | + newSqlBuilder.WriteRune('?') |
| 192 | + newParams = append(newParams, param) |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + return newSqlBuilder.String(), newParams |
| 197 | +} |
0 commit comments