Skip to content

Commit 2113ba6

Browse files
committed
把处理解析命名参数到 ? 占位符 SQL 转换的逻辑封装到独立的包里,以便 MySQL/SQLite 等使用 ? 占位符的驱动能共享逻辑。
1 parent b35b694 commit 2113ba6

10 files changed

Lines changed: 800 additions & 1322 deletions
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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

Comments
 (0)