sq/cli/diff/record.go

249 lines
5.3 KiB
Go
Raw Normal View History

package diff
import (
"bytes"
"context"
"fmt"
"github.com/neilotoole/sq/cli/diff/internal/go-udiff"
"github.com/neilotoole/sq/cli/diff/internal/go-udiff/myers"
"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/cli/output/yamlw"
"github.com/neilotoole/sq/cli/run"
"github.com/neilotoole/sq/libsq"
"github.com/neilotoole/sq/libsq/core/errz"
"github.com/neilotoole/sq/libsq/core/lg"
"github.com/neilotoole/sq/libsq/core/lg/lga"
"github.com/neilotoole/sq/libsq/core/record"
"github.com/neilotoole/sq/libsq/core/stringz"
)
// recordDiff is a container for a single record diff.
//
//nolint:unused
type recordDiff struct {
td1, td2 *tableData
recMeta1, recMeta2 record.Meta
rec1, rec2 record.Record
row int
header string
diff string
}
// findRecordDiff compares the row data in td1 and td2, returning
// a recordDiff instance if there's a difference between the
// equivalent rows. The function stops when it finds the first difference.
//
// NOTE: findRecordDiff (and the functions it calls) are currently unused.
// Instead diff is using a naive implementation that renders all table
// data to text, and then diffs that text. That impl can be horribly
// inefficient for large result sets. findRecordDiff demonstrates one
// possibly better path. The code is left here as a guilty reminder
// to tackle this issue.
//
//nolint:unused
func findRecordDiff(ctx context.Context, ru *run.Run, lines int,
td1, td2 *tableData,
) (*recordDiff, error) {
const chSize = 100
log := lg.FromContext(ctx).
With("a", td1.src.Handle+"."+td1.tblName).
With("b", td2.src.Handle+"."+td2.tblName)
qc := run.NewQueryContext(ru)
query1 := td1.src.Handle + "." + stringz.DoubleQuote(td1.tblName)
query2 := td2.src.Handle + "." + stringz.DoubleQuote(td2.tblName)
errCh := make(chan error, 5)
recw1 := &recWriter{
recCh: make(chan record.Record, chSize),
errCh: errCh,
}
recw2 := &recWriter{
recCh: make(chan record.Record, chSize),
errCh: errCh,
}
gCtx, cancelFn := context.WithCancel(ctx)
go func() {
err := libsq.ExecuteSLQ(gCtx, qc, query1, recw1)
if err != nil {
cancelFn()
select {
case errCh <- err:
default:
}
}
}()
go func() {
err := libsq.ExecuteSLQ(gCtx, qc, query2, recw2)
if err != nil {
cancelFn()
select {
case errCh <- err:
default:
}
}
}()
var (
rec1, rec2 record.Record
i = -1
err error
found bool
)
for {
i++
rec1 = nil
rec2 = nil
err = nil
select {
case err = <-errCh:
case <-ctx.Done():
err = errz.Err(ctx.Err())
case rec1 = <-recw1.recCh:
}
if err != nil {
cancelFn()
log.Error("Table diff", lga.Err, err)
break
}
select {
case err = <-errCh:
case <-ctx.Done():
err = errz.Err(ctx.Err())
case rec2 = <-recw2.recCh:
}
if err != nil {
cancelFn()
log.Error("Table diff", lga.Err, err)
break
}
if rec1 == nil && rec2 == nil {
// End of data, no diff found
break
}
if record.Equal(rec1, rec2) {
continue
}
// We've got a diff!
log.Debug("Found a table diff", "row", i)
found = true
break
}
if err != nil {
return nil, err
}
if !found {
return nil, nil //nolint:nilnil
}
recDiff := &recordDiff{
td1: td1,
td2: td2,
recMeta1: recw1.recMeta,
recMeta2: recw2.recMeta,
rec1: rec1,
rec2: rec2,
row: i,
}
if err = populateRecordDiff(lines, ru.Writers.Printing, recDiff); err != nil {
return nil, err
}
return recDiff, nil
}
//nolint:unused
func populateRecordDiff(lines int, pr *output.Printing, recDiff *recordDiff) error {
pr = pr.Clone()
pr.EnableColor(false)
var (
handleTbl1 = recDiff.td1.src.Handle + "." + recDiff.td1.tblName
handleTbl2 = recDiff.td2.src.Handle + "." + recDiff.td2.tblName
body1, body2 string
err error
)
if body1, err = renderRecord2YAML(pr, recDiff.recMeta1, recDiff.rec1); err != nil {
return err
}
if body2, err = renderRecord2YAML(pr, recDiff.recMeta1, recDiff.rec2); err != nil {
return err
}
edits := myers.ComputeEdits(body1, body2)
recDiff.diff, err = udiff.ToUnified(
handleTbl1,
handleTbl2,
body1,
edits,
lines,
)
if err != nil {
return errz.Err(err)
}
recDiff.header = fmt.Sprintf("sq diff %s %s | .[%d]",
handleTbl1, handleTbl2, recDiff.row)
return nil
}
//nolint:unused
func renderRecord2YAML(pr *output.Printing, recMeta record.Meta, rec record.Record) (string, error) {
if rec == nil {
return "", nil
}
buf := &bytes.Buffer{}
yw := yamlw.NewRecordWriter(buf, pr)
if err := yw.Open(recMeta); err != nil {
return "", err
}
if err := yw.WriteRecords([]record.Record{rec}); err != nil {
return "", err
}
if err := yw.Flush(); err != nil {
return "", err
}
if err := yw.Close(); err != nil {
return "", err
}
return buf.String(), nil
}
var _ libsq.RecordWriter = (*recWriter)(nil)
type recWriter struct {
recCh chan record.Record
errCh chan error
recMeta record.Meta
}
// Open implements libsq.RecordWriter.
func (d *recWriter) Open(_ context.Context, _ context.CancelFunc, recMeta record.Meta,
) (recCh chan<- record.Record, errCh <-chan error, err error) {
d.recMeta = recMeta
return d.recCh, d.errCh, nil
}
// Wait implements libsq.RecordWriter.
func (d *recWriter) Wait() (written int64, err error) {
// We don't actually use Wait(), so just return zero values.
return 0, nil
}