2020-08-06 20:58:47 +03:00
|
|
|
package testh
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"sync"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
|
|
|
"github.com/neilotoole/sq/drivers/sqlite3"
|
2020-08-23 13:42:15 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/kind"
|
2023-11-20 04:06:36 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/lg"
|
|
|
|
"github.com/neilotoole/sq/libsq/core/record"
|
2020-08-23 13:42:15 +03:00
|
|
|
"github.com/neilotoole/sq/libsq/core/sqlz"
|
2020-08-06 20:58:47 +03:00
|
|
|
)
|
|
|
|
|
2022-12-18 11:35:59 +03:00
|
|
|
var (
|
|
|
|
recSinkCache = map[string]*RecordSink{}
|
|
|
|
recSinkMu sync.Mutex
|
|
|
|
)
|
2020-08-06 20:58:47 +03:00
|
|
|
|
|
|
|
// 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.
|
2023-05-22 18:08:14 +03:00
|
|
|
func RecordsFromTbl(tb testing.TB, handle, tbl string) (recMeta record.Meta, recs []record.Record) {
|
2020-08-06 20:58:47 +03:00
|
|
|
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
|
2023-11-19 03:05:48 +03:00
|
|
|
sink, err = th.QuerySQL(src, nil, "SELECT * FROM "+tbl)
|
2020-08-06 20:58:47 +03:00
|
|
|
require.NoError(tb, err)
|
|
|
|
recSinkCache[key] = sink
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make copies so that the caller can mutate their records
|
|
|
|
// without it affecting other callers
|
2023-05-22 18:08:14 +03:00
|
|
|
recMeta = make(record.Meta, len(sink.RecMeta))
|
2022-12-18 03:51:33 +03:00
|
|
|
|
|
|
|
// Don't need to make a deep copy of each FieldMeta because
|
|
|
|
// the type is effectively immutable
|
|
|
|
copy(recMeta, sink.RecMeta)
|
2020-08-06 20:58:47 +03:00
|
|
|
|
2023-05-27 16:57:07 +03:00
|
|
|
recs = record.CloneSlice(sink.Recs)
|
2020-08-06 20:58:47 +03:00
|
|
|
return recMeta, recs
|
|
|
|
}
|
|
|
|
|
2023-05-22 18:08:14 +03:00
|
|
|
// NewRecordMeta builds a new record.Meta instance for testing.
|
|
|
|
func NewRecordMeta(colNames []string, colKinds []kind.Kind) record.Meta {
|
|
|
|
recMeta := make(record.Meta, len(colNames))
|
2020-08-06 20:58:47 +03:00
|
|
|
for i := range colNames {
|
2020-08-23 13:42:15 +03:00
|
|
|
knd := colKinds[i]
|
2023-05-22 18:08:14 +03:00
|
|
|
ct := &record.ColumnTypeData{
|
2020-08-06 20:58:47 +03:00
|
|
|
Name: colNames[i],
|
|
|
|
HasNullable: true,
|
|
|
|
Nullable: true,
|
2020-08-23 13:42:15 +03:00
|
|
|
DatabaseTypeName: sqlite3.DBTypeForKind(knd),
|
|
|
|
ScanType: KindScanType(knd),
|
|
|
|
Kind: knd,
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
2023-07-09 04:34:53 +03:00
|
|
|
recMeta[i] = record.NewFieldMeta(ct, ct.Name)
|
2020-08-06 20:58:47 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return recMeta
|
|
|
|
}
|
|
|
|
|
2020-08-23 13:42:15 +03:00
|
|
|
// KindScanType returns the default scan type for kind. The returned
|
|
|
|
// type is typically a sql.NullType.
|
|
|
|
func KindScanType(knd kind.Kind) reflect.Type {
|
2022-12-18 08:16:10 +03:00
|
|
|
switch knd { //nolint:exhaustive
|
2020-08-23 13:42:15 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2023-05-19 17:24:18 +03:00
|
|
|
|
|
|
|
// 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.
|
2023-05-22 18:08:14 +03:00
|
|
|
RecMeta record.Meta
|
2023-05-19 17:24:18 +03:00
|
|
|
|
|
|
|
// Recs holds the records received via WriteRecords.
|
2023-05-22 18:08:14 +03:00
|
|
|
Recs []record.Record
|
2023-05-19 17:24:18 +03:00
|
|
|
|
|
|
|
// 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.
|
2023-05-22 18:08:14 +03:00
|
|
|
func (r *RecordSink) Open(recMeta record.Meta) error {
|
2023-05-19 17:24:18 +03:00
|
|
|
r.mu.Lock()
|
|
|
|
defer r.mu.Unlock()
|
|
|
|
|
|
|
|
r.RecMeta = recMeta
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// WriteRecords implements libsq.RecordWriter.
|
2023-05-22 18:08:14 +03:00
|
|
|
func (r *RecordSink) WriteRecords(recs []record.Record) error {
|
2023-05-19 17:24:18 +03:00
|
|
|
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
|
|
|
|
}
|