-
Notifications
You must be signed in to change notification settings - Fork 13
/
meta.go
275 lines (232 loc) · 7.27 KB
/
meta.go
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
package record
import (
"database/sql"
"fmt"
"log/slog"
"reflect"
"slices"
"strconv"
"github.com/neilotoole/sq/libsq/core/kind"
)
// FieldMeta is a bit of a strange entity, and in an ideal
// world, it wouldn't exist. It's here because:
//
// - The DB driver impls that sq utilizes (postgres, sqlite, etc)
// often have individual quirks (not reporting nullability etc)
// that necessitate modifying sql.ColumnType so that there's
// consistent behavior across the drivers.
//
// - We wanted to retain (and supplement) the method set of
// sql.ColumnType (basically, use the same "interface", even
// though sql.ColumnType is a struct, not interface) so that
// devs don't need to learn a whole new thing.
//
// - For that reason, stdlib sql.ColumnType needs to be
// supplemented with kind.Kind, and there needs to
// be a mechanism for modifying sql.ColumnType's fields.
//
// - But sql.ColumnType is sealed (its fields cannot be changed
// from outside its package).
//
// - Hence this construct where we have FieldMeta (which
// abstractly is an adapter around sql.ColumnType) and also
// a data holder struct (ColumnDataType), which permits the
// mutation of the column type fields.
//
// Likely there's a better design available than this one,
// but it suffices.
type FieldMeta struct {
data *ColumnTypeData
mungedName string
}
// NewFieldMeta returns a new instance backed by the data arg.
// If mungedName is empty, ColumnTypeData.Name is used.
func NewFieldMeta(data *ColumnTypeData, mungedName string) *FieldMeta {
if mungedName == "" {
mungedName = data.Name
}
return &FieldMeta{data: data, mungedName: mungedName}
}
// String returns a log-debug friendly representation.
func (fm *FieldMeta) String() string {
if fm == nil {
return "<nil>"
}
nullMsg := "?"
if fm.data.HasNullable {
nullMsg = strconv.FormatBool(fm.data.Nullable)
}
return fmt.Sprintf(
"%s|%s|%s|%s|%s|%s",
fm.data.Name,
fm.mungedName,
fm.data.Kind,
fm.data.DatabaseTypeName,
fm.ScanType(),
nullMsg,
)
}
// Name is documented by sql.ColumnType.Name.
func (fm *FieldMeta) Name() string {
return fm.data.Name
}
// MungedName returns the (possibly-munged) column name.
// This value is what should be used for outputting the col name.
// This exists largely to handle the case of duplicate col names
// in a result set, e.g. when doing a JOIN on tables with
// identically-named columns. But typically this value is the same
// as that returned by FieldMeta.Name.
func (fm *FieldMeta) MungedName() string {
return fm.mungedName
}
// Length is documented by sql.ColumnType.Written.
func (fm *FieldMeta) Length() (length int64, ok bool) {
return fm.data.Length, fm.data.HasLength
}
// DecimalSize is documented by sql.ColumnType.DecimalSize.
func (fm *FieldMeta) DecimalSize() (precision, scale int64, ok bool) {
return fm.data.Precision, fm.data.Scale, fm.data.HasPrecisionScale
}
// ScanType is documented by sql.ColumnType.ScanType.
func (fm *FieldMeta) ScanType() reflect.Type {
return fm.data.ScanType
}
// Nullable is documented by sql.ColumnType.Nullable.
func (fm *FieldMeta) Nullable() (nullable, ok bool) {
return fm.data.Nullable, fm.data.HasNullable
}
// DatabaseTypeName is documented by sql.ColumnType.DatabaseTypeName.
func (fm *FieldMeta) DatabaseTypeName() string {
return fm.data.DatabaseTypeName
}
// Kind returns the data kind for the column.
func (fm *FieldMeta) Kind() kind.Kind {
return fm.data.Kind
}
// Meta is a slice of *FieldMeta, encapsulating the metadata
// for a record.
type Meta []*FieldMeta
// Names returns the column names. These are the col names from
// the database. See also: MungedNames.
func (rm Meta) Names() []string {
names := make([]string, len(rm))
for i, col := range rm {
names[i] = col.Name()
}
return names
}
// MungedNames returns the munged column names, which may be
// the same as those returned from Meta.Names.
func (rm Meta) MungedNames() []string {
names := make([]string, len(rm))
for i, col := range rm {
names[i] = col.MungedName()
}
return names
}
// NewScanRow returns a new []any that can be scanned
// into by sql.Rows.Scan.
func (rm Meta) NewScanRow() []any {
dests := make([]any, len(rm))
for i, col := range rm {
if col.data.ScanType == nil {
// If there's no scan type set, fall back on *any
dests[i] = new(any)
continue
}
val := reflect.New(col.data.ScanType)
dests[i] = val.Interface()
}
return dests
}
// Kinds returns the data kinds for the record.
func (rm Meta) Kinds() []kind.Kind {
kinds := make([]kind.Kind, len(rm))
for i, col := range rm {
kinds[i] = col.Kind()
}
return kinds
}
// ScanTypes returns the scan types for the record.
func (rm Meta) ScanTypes() []reflect.Type {
scanTypes := make([]reflect.Type, len(rm))
for i, col := range rm {
scanTypes[i] = col.ScanType()
}
return scanTypes
}
// LogValue implements slog.LogValuer.
func (rm Meta) LogValue() slog.Value {
if len(rm) == 0 {
return slog.Value{}
}
a := make([]string, len(rm))
for i := range rm {
a[i] = rm[i].String()
}
return slog.AnyValue(a)
}
// Equalish returns true if rm and other are equal in the most relevant aspects.
// Specifically, they're equal if their field kinds are equal, and have the
// same munged names.
func (rm Meta) Equalish(other Meta) bool {
switch {
case rm == nil && other == nil:
return true
case other == nil || rm == nil:
return false
case len(rm) != len(other):
return false
case &rm == &other:
return true
case !slices.Equal(rm.Kinds(), other.Kinds()):
return false
case !slices.Equal(rm.MungedNames(), other.MungedNames()):
return false
}
return true
}
// ColumnTypeData contains the same data as sql.ColumnType
// as well SQ's derived data kind. This type exists with
// exported fields instead of methods (as on sql.ColumnType)
// due to the need to work with the fields for testing, and
// also because for some drivers it's useful to twiddle with
// the scan type.
//
// This is all a bit ugly.
type ColumnTypeData struct {
ScanType reflect.Type `json:"scan_type"`
Name string `json:"name"`
DatabaseTypeName string `json:"database_type_name"`
Length int64 `json:"length"`
Precision int64 `json:"precision"`
Scale int64 `json:"scale"`
Kind kind.Kind `json:"kind"`
HasNullable bool `json:"has_nullable"`
HasLength bool `json:"has_length"`
HasPrecisionScale bool `json:"has_precision_scale"`
Nullable bool `json:"nullable"`
}
// NewColumnTypeData returns a new instance with field values
// taken from col, supplemented with the kind param.
func NewColumnTypeData(col *sql.ColumnType, knd kind.Kind) *ColumnTypeData {
ct := &ColumnTypeData{
Name: col.Name(),
DatabaseTypeName: col.DatabaseTypeName(),
ScanType: col.ScanType(),
Kind: knd,
}
ct.Nullable, ct.HasNullable = col.Nullable()
ct.Length, ct.HasLength = col.Length()
ct.Precision, ct.Scale, ct.HasPrecisionScale = col.DecimalSize()
return ct
}
// SetKindIfUnknown sets meta[i].kind to k, iff the kind is
// currently kind.Unknown or kind.Null. This function can be used to set
// the kind after-the-fact, which is useful for some databases
// that don't always return sufficient type info upfront.
func SetKindIfUnknown(meta Meta, i int, k kind.Kind) {
if meta[i].data.Kind == kind.Unknown || meta[i].data.Kind == kind.Null {
meta[i].data.Kind = k
}
}