-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.go
More file actions
359 lines (331 loc) · 12.6 KB
/
Copy pathutils.go
File metadata and controls
359 lines (331 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
package dba
import (
"database/sql/driver"
"fmt"
"reflect"
"sort"
"strings"
"sync"
"time"
"github.com/jmoiron/sqlx/reflectx"
)
// Map transforms each element of a slice using fn and returns a new slice.
func Map[T any, R any](slice []T, fn func(T) R) []R {
result := make([]R, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// IndexBy converts a slice into a map keyed by fn(element).
// Returns an error if duplicate keys are found.
func IndexBy[T any, K comparable](slice []T, fn func(T) K) (map[K]T, error) {
m := make(map[K]T, len(slice))
for _, v := range slice {
key := fn(v)
if _, ok := m[key]; ok {
return nil, fmt.Errorf("dba: IndexBy duplicate key %v", key)
}
m[key] = v
}
return m, nil
}
// GroupBy groups slice elements by fn(element): 1 key → N values.
func GroupBy[T any, K comparable](slice []T, fn func(T) K) map[K][]T {
m := make(map[K][]T, len(slice))
for _, v := range slice {
key := fn(v)
m[key] = append(m[key], v)
}
return m
}
// FetchIndexed fetches all rows and indexes them into a map keyed by
// key(v) — 查询版 IndexBy。重复键报错 (与 IndexBy 一致)。
func (d *SQL) FetchIndexed[K comparable, V any](key func(V) K) (map[K]V, error) {
list, err := d.FetchList[V]()
if err != nil {
return nil, err
}
return IndexBy(list, key)
}
// FetchGrouped fetches all rows and groups them into a map keyed by
// key(v) — 查询版 GroupBy。每个键对应一个值切片 (保持查询顺序)。
func (d *SQL) FetchGrouped[K comparable, V any](key func(V) K) (map[K][]V, error) {
list, err := d.FetchList[V]()
if err != nil {
return nil, err
}
return GroupBy(list, key), nil
}
// FetchPage fetches a page of rows and the total count.
//
// Contract: the query must contain the ${F:...} slot (or bare ${F}) on the
// main chain — the F slot is substituted with COUNT(1) for the count query.
// Constraints: no GROUP BY / DISTINCT in the F slot content (the count query
// reuses the same template).
//
// ORDER BY must live in the ${order:...} slot: the count query clears it with
// Var(O, "") — 严格方言 (PG/mssql) 下聚合查询带 ORDER BY 源列是硬错误,
// 且 SQL Server 的 OFFSET...FETCH 强制数据查询带 ORDER BY, 两者只能靠
// ${order} 槽同时满足。Example:
//
// q.Add("SELECT ... WHERE x ${order:ORDER BY id DESC}") // then q.FetchPage[User](1, 20)
//
// 分页子句默认 SQL:2008 标准 (OFFSET...FETCH), mysql/sqlite 系自动切
// LIMIT/OFFSET, 可用 Pager() 覆盖; SQL Server/Oracle/DB2 系强制 ORDER BY。
// page/size 必须 >= 1, 否则报错 (不静默钳制, 尽早暴露调用方的参数 bug)。
// The total==0 case skips the data query entirely.
func (d *SQL) FetchPage[T any](page, size int) ([]T, int64, error) {
if page < 1 || size < 1 {
return nil, 0, fmt.Errorf("dba: fetch page: page & size must be >= 1, got (%d, %d)", page, size)
}
// F 槽必须在主链 (${F:...} 或裸 ${F}): 探测两种形态, 避免 ${From} 误报
hasF := false
for _, node := range d.mainNodes {
if strings.Contains(node.Text, "${"+F+":") || strings.Contains(node.Text, "${"+F+"}") {
hasF = true
break
}
}
if !hasF {
return nil, 0, fmt.Errorf("dba: fetch page: query requires ${%s:...} (main chain)", F)
}
// count 查询: F → COUNT(1), 并清空排序槽。
// 注意: 这不是纯性能优化 (省一次无意义排序) —— 严格方言下
// (PostgreSQL / SQL Server / 严格模式 MySQL) 聚合查询的 ORDER BY
// 引用非聚合源列是硬错误 (PG: "column must appear in the GROUP BY
// clause"; mssql: "is invalid in the order by clause")。SQL Server 的
// OFFSET...FETCH 又强制数据查询带 ORDER BY —— 两者只能靠 ${order} 槽
// 同时满足: 数据查询保留, count 查询清空。
total, err := d.Var(F, "COUNT(1)").Var(O, "").FetchOne[int64]()
if err != nil {
return nil, 0, err
}
var items []T
if *total == 0 {
return items, *total, nil
}
offset := (page - 1) * size
items, err = d.Add(d.pager("#{1}", "#{2}"), size, offset).FetchList[T]()
return items, *total, err
}
// IsOk returns true if v is non-nil, non-blank string, or non-empty
// slice/array/map.
func IsOk(v any) bool {
if v == nil {
return false
}
switch val := v.(type) {
case string:
return strings.TrimSpace(val) != ""
}
rv := reflect.ValueOf(v)
for rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
if rv.IsNil() {
return false
}
rv = rv.Elem()
}
switch rv.Kind() {
case reflect.String:
return strings.TrimSpace(rv.String()) != ""
case reflect.Slice, reflect.Array, reflect.Map:
return rv.Len() > 0
case reflect.Invalid:
return false
default:
return true
}
}
// ColumnsAndValues converts a struct or map into column names and bind values.
//
// Struct strategy: fieldList (derived from the reflectx TypeMap — the same
// mapper used by #{name} resolution and row scanning) builds the field list,
// with dba's atomicity decision (isAtomicColumn) applied per field:
// - atomic types (driver.Valuer implementers / time.Time and convertible
// aliases / Node) collapse to a single column;
// - structs (embedded or plain, value or pointer) expand recursively;
// - other basic types / []byte are single columns.
//
// Collected field values pass through normalizeBindValue before entering vals.
// The map branch does no normalization (same level as Add arguments: bind
// exactly what the caller passed).
func ColumnsAndValues(model any, omitempty bool) ([]string, []any, error) {
rv := reflect.ValueOf(model)
for rv.Kind() == reflect.Pointer || rv.Kind() == reflect.Interface {
if rv.IsNil() {
return []string{}, []any{}, nil
}
rv = rv.Elem()
}
// ── Map branch ──────────────────────────────────────────
if rv.Kind() == reflect.Map {
if rv.Type().Key().Kind() != reflect.String {
return nil, nil, fmt.Errorf("dba: ToKV map key must be string, got %s", rv.Type().Key().Kind())
}
keys := rv.MapKeys()
sort.Slice(keys, func(i, j int) bool {
return keys[i].String() < keys[j].String()
})
result := make([]string, len(keys))
vals := make([]any, len(keys))
for i, k := range keys {
result[i] = k.String()
vals[i] = rv.MapIndex(k).Interface()
}
return result, vals, nil
}
// ── Struct branch ───────────────────────────────────────
if rv.Kind() != reflect.Struct {
if !rv.IsValid() {
return nil, nil, fmt.Errorf("dba: ToKV expects struct or map[string]any, got nil")
}
return nil, nil, fmt.Errorf("dba: ToKV expects struct or map[string]any, got %s", rv.Kind())
}
fields := fieldList(rv.Type())
keys := make([]string, 0, len(fields))
vals := make([]any, 0, len(fields))
for _, f := range fields {
val := fieldByPath(rv, f.path)
// omitempty 判断先于归一化: 指针只看 nilness (逃生舱语义)
if omitempty && f.omitempty && isZeroValue(val) {
continue
}
keys = append(keys, f.key)
vals = append(vals, normalizeBindValue(val).Interface())
}
return keys, vals, nil
}
// normalizeBindValue 归一化 struct 字段的绑定值 (omitempty 判断之后调用):
//
// 1. 非 nil 指针解引用一级 — 但指针自身实现 Valuer 的除外 (指针接收者的
// Value 方法只在 *T 上, 解引用会剥掉它, 导致 driver 报 unsupported)。
// nil 指针原样保留 (Bind 的 *Node nil case / driver 按 NULL 处理)。
// 注: 主流 driver 的 DefaultParameterConverter 本也会解指针; 这里自行
// 解引用是为了 *Node → Node 直达 Bind 内联, 并且不依赖各 driver
// converter 行为一致 (自定义 NamedValueChecker 的 driver 可能不同)。
//
// 2. time.Time 的可转换别名 (type MyTime time.Time, 自身无 Valuer) 转换为
// time.Time — isAtomicColumn 按 ConvertibleTo 判它为单列, 但别名类型
// 本身不是 driver 原生类型也无 Valuer, 不转换绑不进去。
// time.Time 本尊、Node、Valuer 实现者均不转换 (各有自己的绑定路径)。
func normalizeBindValue(val reflect.Value) reflect.Value {
if val.Kind() == reflect.Pointer {
if val.IsNil() || val.Type().Implements(valuableType) {
return val
}
val = val.Elem()
}
t := val.Type()
if t.Kind() == reflect.Struct && t != timeType && t != nodeType &&
!t.Implements(valuableType) && t.ConvertibleTo(timeType) {
val = val.Convert(timeType)
}
return val
}
// fieldByPath 沿索引路径取值, 自动穿越指针/接口中间层。
// 与 reflectx.FieldByIndexesReadOnly 的区别: nil 指针中间层返回目标字段类型的零值,
// 不 panic (展开 nil *struct 字段时必需)。
func fieldByPath(rv reflect.Value, path []int) reflect.Value {
// 先沿类型推导目标字段类型 (穿越指针/接口)
t := rv.Type()
for _, i := range path {
for t.Kind() == reflect.Pointer || t.Kind() == reflect.Interface {
t = t.Elem()
}
t = t.Field(i).Type
}
// 再沿值取字段 (nil 指针/接口 → 返回目标零值)
cur := rv
for _, i := range path {
for cur.Kind() == reflect.Pointer || cur.Kind() == reflect.Interface {
if cur.IsNil() {
return reflect.Zero(t)
}
cur = cur.Elem()
}
cur = cur.Field(i)
}
return cur
}
// kvField 一个待写入列: 列名 + 取值索引路径 + omitempty 选项。
type kvField struct {
key string
path []int
omitempty bool
}
var fieldListCache sync.Map // reflect.Type → []kvField
// fieldList 从 reflectx TypeMap 派生写入列清单。
//
// 命名/嵌入/跳过规则全部由 mapper (与 #{name} 解析、List/Get 扫描共用同一
// 实例) 单点定义: db tag 名优先且原样使用, 无 tag 用 mapFunc(ToLower),
// db:"-" 排除, unexported 跳过 (匿名嵌入类型除外)。
//
// 在 reflectx 展开树上叠加 dba 的原子判定 (isAtomicColumn):
// Valuer / time.Time 及可转换别名 / Node 收束为单列, 不下钻子字段;
// 其余 struct 字段 (值/指针/匿名嵌入) 递归展开, 列名取子字段自身的
// 映射名 fi.Name — 不用 fi.Path 的 "a.b" 点路径 (那是扫描语义,
// INSERT 列名没有前缀概念), 这是与 reflectx 默认展开的唯一分歧点。
//
// 与旧自建遍历的行为差异 (有意为之): 显式 db tag 名不再 ToLower —
// 此前 INSERT 列名强制小写, 而 #{name} 解析和行扫描按 reflectx 原样,
// 同一 tag 两套规则; 现在三处收敛为一套。
func fieldList(t reflect.Type) []kvField {
if v, ok := fieldListCache.Load(t); ok {
return v.([]kvField)
}
tm := mapper.TypeMap(t) // reflectx 内部按类型缓存, 与扫描共享
var out []kvField
var walk func(fis []*reflectx.FieldInfo)
walk = func(fis []*reflectx.FieldInfo) {
for _, fi := range fis {
if fi == nil {
// Children 按字段序号占位: 被跳过的字段
// (unexported / db:"-") 留 nil 洞
continue
}
ft := fi.Field.Type
for ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && !isAtomicColumn(ft) {
walk(fi.Children) // 非原子 struct: 展开子字段
continue
}
_, omit := fi.Options["omitempty"] // reflectx 已解析 tag 选项
out = append(out, kvField{
key: fi.Name, // 叶子映射名 (不是 fi.Path)
path: fi.Index, // 根起始的完整索引路径
omitempty: omit,
})
}
}
walk(tm.Tree.Children)
fieldListCache.Store(t, out)
return out
}
// timeType time.Time 是 database/sql 原生参数类型 (不实现 driver.Valuer)。
var timeType = reflect.TypeFor[time.Time]()
// valuableType driver.Valuer 接口类型。
var valuableType = reflect.TypeFor[driver.Valuer]()
// nodeType Node 的反射类型。
var nodeType = reflect.TypeFor[Node]()
// isAtomicColumn 判断 struct 类型是否应整体作为单列写入:
// 1. 实现 driver.Valuer (sql.NullString/NullInt64/自定义类型)
// 2. time.Time 及其可转换别名 — database/sql 原生参数类型, 不实现 Valuer;
// 用 ConvertibleTo 而非 ==, 覆盖 type MyTime time.Time 这类别名 (对齐 GORM schema.ParseField)
// 3. Node — "参数即子树"的值, 整体收束后原样流到 Bind 内联 (不展开为
// text/args 两个子列)
func isAtomicColumn(t reflect.Type) bool {
return t == nodeType || t.ConvertibleTo(timeType) || t.Implements(valuableType)
}
// isZeroValue omitempty 语义的零值判断。
// 指针只判 nilness 不解引用: nil = 未设置 (跳过), 非 nil = 显式赋值
// (即使指向零值也保留) — 这是 omitempty 字段写入零值的逃生舱,
// 与 encoding/json 的 omitempty 约定一致。
func isZeroValue(v reflect.Value) bool {
if v.Kind() == reflect.Pointer {
return v.IsNil()
}
return v.IsZero()
}