mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-20 06:31:32 +03:00
c7bba4dfe4
* go1.21: changes to support slog as part of stdlib * Removed accidentially checked-in line of code * Fixed minor linting issues; reenable typecheck * go1.21: switched to stdlib slices pkg
247 lines
6.7 KiB
Go
247 lines
6.7 KiB
Go
package record
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
"reflect"
|
|
"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 {
|
|
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.String(),
|
|
fm.data.DatabaseTypeName,
|
|
fm.ScanType().String(),
|
|
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.Length.
|
|
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)
|
|
}
|
|
|
|
// 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 {
|
|
Name string `json:"name"`
|
|
|
|
HasNullable bool `json:"has_nullable"`
|
|
HasLength bool `json:"has_length"`
|
|
HasPrecisionScale bool `json:"has_precision_scale"`
|
|
|
|
Nullable bool `json:"nullable"`
|
|
Length int64 `json:"length"`
|
|
DatabaseTypeName string `json:"database_type_name"`
|
|
Precision int64 `json:"precision"`
|
|
Scale int64 `json:"scale"`
|
|
ScanType reflect.Type `json:"scan_type"`
|
|
|
|
Kind kind.Kind `json:"kind"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|