sq/testh/record.go
Neil O'Toole 6ca26f4e4f
Column rename: template now has Alpha field. (#285)
* wip: refactor col name mungeing

* Finished refactoring FieldMeta

* Renamed tpl .AlphaIndex to .Alpha

* wip: debugging source config override

* Source config override passing tests

* CHANGELOG update
2023-07-08 19:34:53 -06:00

185 lines
4.3 KiB
Go

package testh
import (
"fmt"
"reflect"
"sync"
"testing"
"time"
"github.com/neilotoole/sq/libsq/core/record"
"github.com/neilotoole/sq/libsq/core/lg"
"github.com/stretchr/testify/require"
"github.com/neilotoole/sq/drivers/sqlite3"
"github.com/neilotoole/sq/libsq/core/kind"
"github.com/neilotoole/sq/libsq/core/sqlz"
)
var (
recSinkCache = map[string]*RecordSink{}
recSinkMu sync.Mutex
)
// RecordsFromTbl returns a cached copy of all records from handle.tbl.
// The function performs a "SELECT * FROM tbl" and caches (in a package
// variable) the returned recs and recMeta for subsequent calls. Thus
// if the underlying data source records are modified, the returned records
// may be inconsistent.
//
// This function effectively exists to speed up testing times.
func RecordsFromTbl(tb testing.TB, handle, tbl string) (recMeta record.Meta, recs []record.Record) {
recSinkMu.Lock()
defer recSinkMu.Unlock()
key := fmt.Sprintf("#rec_sink__%s__%s", handle, tbl)
sink, ok := recSinkCache[key]
if !ok {
th := New(tb)
th.Log = lg.Discard()
src := th.Source(handle)
var err error
sink, err = th.QuerySQL(src, "SELECT * FROM "+tbl)
require.NoError(tb, err)
recSinkCache[key] = sink
}
// Make copies so that the caller can mutate their records
// without it affecting other callers
recMeta = make(record.Meta, len(sink.RecMeta))
// Don't need to make a deep copy of each FieldMeta because
// the type is effectively immutable
copy(recMeta, sink.RecMeta)
recs = record.CloneSlice(sink.Recs)
return recMeta, recs
}
// NewRecordMeta builds a new record.Meta instance for testing.
func NewRecordMeta(colNames []string, colKinds []kind.Kind) record.Meta {
recMeta := make(record.Meta, len(colNames))
for i := range colNames {
knd := colKinds[i]
ct := &record.ColumnTypeData{
Name: colNames[i],
HasNullable: true,
Nullable: true,
DatabaseTypeName: sqlite3.DBTypeForKind(knd),
ScanType: KindScanType(knd),
Kind: knd,
}
recMeta[i] = record.NewFieldMeta(ct, ct.Name)
}
return recMeta
}
// KindScanType returns the default scan type for kind. The returned
// type is typically a sql.NullType.
func KindScanType(knd kind.Kind) reflect.Type {
switch knd { //nolint:exhaustive
default:
return sqlz.RTypeNullString
case kind.Text, kind.Decimal:
return sqlz.RTypeNullString
case kind.Int:
return sqlz.RTypeNullInt64
case kind.Bool:
return sqlz.RTypeNullBool
case kind.Float:
return sqlz.RTypeNullFloat64
case kind.Bytes:
return sqlz.RTypeBytes
case kind.Datetime:
return sqlz.RTypeNullTime
case kind.Date:
return sqlz.RTypeNullTime
case kind.Time:
return sqlz.RTypeNullTime
}
}
// RecordSink is an impl of output.RecordWriter that
// captures invocations of that interface.
type RecordSink struct {
mu sync.Mutex
// RecMeta holds the recMeta received via Open.
RecMeta record.Meta
// Recs holds the records received via WriteRecords.
Recs []record.Record
// Closed tracks the times Close was invoked.
Closed []time.Time
// Flushed tracks the times Flush was invoked.
Flushed []time.Time
}
// Result returns the first (and only) value returned from
// a query like "SELECT COUNT(*) FROM actor". It is effectively
// the same as RecordSink.Recs[0][0]. The function will panic
// if there is no appropriate result.
func (r *RecordSink) Result() any {
if len(r.Recs) == 0 || len(r.RecMeta) == 0 {
panic("record sink has no data")
}
if len(r.RecMeta) != 1 {
panic(fmt.Sprintf("record sink data should have 1 cold, but got %d", len(r.RecMeta)))
}
if len(r.Recs) != 1 {
panic(fmt.Sprintf("record sink should have 1 record, but got %d", len(r.Recs)))
}
return r.Recs[0][0]
}
// Open implements libsq.RecordWriter.
func (r *RecordSink) Open(recMeta record.Meta) error {
r.mu.Lock()
defer r.mu.Unlock()
r.RecMeta = recMeta
return nil
}
// WriteRecords implements libsq.RecordWriter.
func (r *RecordSink) WriteRecords(recs []record.Record) error {
r.mu.Lock()
defer r.mu.Unlock()
r.Recs = append(r.Recs, recs...)
return nil
}
// Flush implements libsq.RecordWriter.
func (r *RecordSink) Flush() error {
r.mu.Lock()
defer r.mu.Unlock()
r.Flushed = append(r.Flushed, time.Now())
return nil
}
// Close implements libsq.RecordWriter.
func (r *RecordSink) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
r.Closed = append(r.Closed, time.Now())
return nil
}