mirror of
https://github.com/neilotoole/sq.git
synced 2024-11-23 19:33:22 +03:00
parent
78c3cfc77d
commit
2cca8a51b2
27
CHANGELOG.md
27
CHANGELOG.md
@ -9,8 +9,34 @@ Breaking changes are annotated with ☢️, and alpha/beta features with 🐥.
|
||||
|
||||
## Upcoming
|
||||
|
||||
This release features significant improvements to [`sq diff`](https://sq.io/docs/diff).
|
||||
|
||||
## Added
|
||||
|
||||
- Previously `sq diff --data` diffed every row, which could get crazy
|
||||
with a large table. Now the command stops after N differences, where N is controlled by
|
||||
the `--stop` flag, or the new config option [`diff.stop`](https://sq.io/docs/config#diffstop).
|
||||
The default stop-after value is `3`; set to `0` to show all differences.
|
||||
|
||||
```shell
|
||||
# Stop on first difference
|
||||
$ sq diff @prod.actor @staging.actor --data --stop 1
|
||||
|
||||
# Stop after 5 differences, using the -n shorthand flag
|
||||
$ sq diff @prod.actor @staging.actor --data -n 5
|
||||
```
|
||||
- [#353]: The performance of `sq diff` has been significantly improved. There's still more to do.
|
||||
|
||||
## Changed
|
||||
|
||||
- ☢️ Previously, `sq diff` only exited non-zero on an error. Now, `sq diff` exits `0` when no differences,
|
||||
exits `1` if differences are found, and exits `2` on any error.
|
||||
This aligns with the behavior of [GNU diff](https://www.gnu.org/software/diffutils/manual/):
|
||||
|
||||
```text
|
||||
Exit status is 0 if inputs are the same, 1 if different, 2 if trouble.
|
||||
```
|
||||
|
||||
- Minor fiddling with the color scheme for some command output.
|
||||
|
||||
|
||||
@ -1131,6 +1157,7 @@ make working with lots of sources much easier.
|
||||
[#335]: https://github.com/neilotoole/sq/issues/335
|
||||
[#338]: https://github.com/neilotoole/sq/issues/338
|
||||
[#340]: https://github.com/neilotoole/sq/pull/340
|
||||
[#353]: https://github.com/neilotoole/sq/pull/353
|
||||
|
||||
|
||||
[v0.15.2]: https://github.com/neilotoole/sq/releases/tag/v0.15.2
|
||||
|
@ -20,7 +20,6 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -56,13 +55,6 @@ const (
|
||||
msgSrcEmptyTableName = "source has empty table name"
|
||||
)
|
||||
|
||||
// errNoMsg is a sentinel error indicating that a command
|
||||
// has failed (and thus the program should exit with a non-zero
|
||||
// code), but no error message should be printed.
|
||||
// This is useful in the case where any error information may
|
||||
// already have been printed as part of the command output.
|
||||
var errNoMsg = errors.New("")
|
||||
|
||||
// Execute builds a Run using ctx and default
|
||||
// settings, and invokes ExecuteWith.
|
||||
func Execute(ctx context.Context, stdin *os.File, stdout, stderr io.Writer, args []string) error {
|
||||
|
@ -27,6 +27,15 @@ var OptDiffNumLines = options.NewInt(
|
||||
options.TagOutput,
|
||||
)
|
||||
|
||||
var OptDiffStopAfter = options.NewInt(
|
||||
"diff.stop",
|
||||
&options.Flag{Name: "stop", Short: 'n'},
|
||||
3,
|
||||
"Stop after <n> differences",
|
||||
`Stop after <n> differences are found. If n <= 0, no limit is applied.`,
|
||||
options.TagOutput,
|
||||
)
|
||||
|
||||
var OptDiffHunkMaxSize = options.NewInt(
|
||||
"diff.hunk.max-size",
|
||||
nil,
|
||||
@ -98,8 +107,7 @@ source overview, schema, and table row counts. Table row data is not compared.
|
||||
When comparing tables ("table diff"), the default is to diff table schema and
|
||||
row counts. Table row data is not compared.
|
||||
|
||||
Use flags to specify the elements you want to compare. The available
|
||||
elements are:
|
||||
Use flags to specify the modes you want to compare. The available modes are:
|
||||
|
||||
--overview source metadata, without schema (source diff only)
|
||||
--dbprops database/server properties (source diff only)
|
||||
@ -108,13 +116,14 @@ elements are:
|
||||
--data row data values
|
||||
--all all of the above
|
||||
|
||||
Flag --data diffs the values of each row in the compared tables. Use with
|
||||
caution with large tables.
|
||||
Flag --data diffs the values of each row in the compared tables, until the stop
|
||||
limit is reached. Use the --stop (-n) flag or the diff.stop config option to
|
||||
specify the stop limit. The default is 3.
|
||||
|
||||
Use --format with --data to specify the format to render the diff records.
|
||||
Line-based formats (e.g. "text" or "jsonl") are often the most ergonomic,
|
||||
although "yaml" may be preferable for comparing column values. The
|
||||
available formats are:
|
||||
although "yaml" may be preferable for comparing column values. The available
|
||||
formats are:
|
||||
|
||||
text, csv, tsv,
|
||||
json, jsona, jsonl,
|
||||
@ -132,15 +141,16 @@ Note that --overview and --dbprops only apply to source diffs, not table diffs.
|
||||
Flag --unified (-U) controls the number of lines to show surrounding a diff.
|
||||
The default (3) can be changed via:
|
||||
|
||||
$ sq config set diff.lines N`,
|
||||
$ sq config set diff.lines N
|
||||
|
||||
Exit status is 0 if inputs are the same, 1 if different, 2 on any error.`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: (&handleTableCompleter{
|
||||
handleRequired: true,
|
||||
max: 2,
|
||||
}).complete,
|
||||
RunE: execDiff,
|
||||
Example: `
|
||||
Metadata diff
|
||||
Example: ` Metadata diff
|
||||
-------------
|
||||
|
||||
# Diff sources (compare default elements).
|
||||
@ -173,17 +183,18 @@ The default (3) can be changed via:
|
||||
Row data diff
|
||||
-------------
|
||||
|
||||
# Compare data in the actor tables.
|
||||
$ sq diff @prod/sakila.actor @staging/sakila.actor --data
|
||||
# Compare data in the actor tables, stopping at the first difference.
|
||||
$ sq diff @prod/sakila.actor @staging/sakila.actor --data --stop 1
|
||||
|
||||
# Compare data in the actor tables, but output in JSONL.
|
||||
$ sq diff @prod/sakila.actor @staging/sakila.actor --data --format jsonl
|
||||
|
||||
# Compare data in all tables and views. Caution: may be slow.
|
||||
$ sq diff @prod/sakila @staging/sakila --data`,
|
||||
$ sq diff @prod/sakila @staging/sakila --data --stop 0`,
|
||||
}
|
||||
|
||||
addOptionFlag(cmd.Flags(), OptDiffNumLines)
|
||||
addOptionFlag(cmd.Flags(), OptDiffStopAfter)
|
||||
addOptionFlag(cmd.Flags(), OptDiffDataFormat)
|
||||
|
||||
cmd.Flags().BoolP(flag.DiffOverview, flag.DiffOverviewShort, false, flag.DiffOverviewUsage)
|
||||
@ -209,10 +220,23 @@ The default (3) can be changed via:
|
||||
}
|
||||
|
||||
// execDiff compares sources or tables.
|
||||
func execDiff(cmd *cobra.Command, args []string) error {
|
||||
func execDiff(cmd *cobra.Command, args []string) (err error) {
|
||||
ctx := cmd.Context()
|
||||
ru := run.FromContext(ctx)
|
||||
|
||||
var foundDiffs bool
|
||||
defer func() {
|
||||
// From GNU diff help:
|
||||
// > Exit status is 0 if inputs are the same, 1 if different, 2 if trouble.
|
||||
switch {
|
||||
case err != nil:
|
||||
err = errz.WithExitCode(err, 2)
|
||||
case foundDiffs:
|
||||
// We want to exit 1 if diffs were found.
|
||||
err = errz.WithExitCode(errz.ErrNoMsg, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
handle1, table1, err := source.ParseTableHandle(args[0])
|
||||
if err != nil {
|
||||
return errz.Wrapf(err, "invalid input (1st arg): %s", args[0])
|
||||
@ -248,6 +272,7 @@ func execDiff(cmd *cobra.Command, args []string) error {
|
||||
diffCfg := &diff.Config{
|
||||
Run: ru,
|
||||
Lines: OptDiffNumLines.Get(o),
|
||||
StopAfter: OptDiffStopAfter.Get(o),
|
||||
HunkMaxSize: OptDiffHunkMaxSize.Get(o),
|
||||
Printing: ru.Writers.OutPrinting.Clone(),
|
||||
Colors: ru.Writers.OutPrinting.Diff.Clone(),
|
||||
@ -261,20 +286,22 @@ func execDiff(cmd *cobra.Command, args []string) error {
|
||||
|
||||
switch {
|
||||
case table1 == "" && table2 == "":
|
||||
diffCfg.Elements = getDiffSourceElements(cmd)
|
||||
return diff.ExecSourceDiff(ctx, diffCfg, src1, src2)
|
||||
diffCfg.Modes = getDiffSourceElements(cmd)
|
||||
foundDiffs, err = diff.ExecSourceDiff(ctx, diffCfg, src1, src2)
|
||||
case table1 == "" || table2 == "":
|
||||
return errz.Errorf("invalid args: both must be either @HANDLE or @HANDLE.TABLE")
|
||||
default:
|
||||
diffCfg.Elements = getDiffTableElements(cmd)
|
||||
return diff.ExecTableDiff(ctx, diffCfg, src1, table1, src2, table2)
|
||||
diffCfg.Modes = getDiffTableElements(cmd)
|
||||
foundDiffs, err = diff.ExecTableDiff(ctx, diffCfg, src1, table1, src2, table2)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getDiffSourceElements(cmd *cobra.Command) *diff.Elements {
|
||||
func getDiffSourceElements(cmd *cobra.Command) *diff.Modes {
|
||||
if !isAnyDiffElementsFlagChanged(cmd) {
|
||||
// Default
|
||||
return &diff.Elements{
|
||||
return &diff.Modes{
|
||||
Overview: true,
|
||||
DBProperties: false,
|
||||
Schema: true,
|
||||
@ -284,7 +311,7 @@ func getDiffSourceElements(cmd *cobra.Command) *diff.Elements {
|
||||
}
|
||||
|
||||
if cmdFlagChanged(cmd, flag.DiffAll) {
|
||||
return &diff.Elements{
|
||||
return &diff.Modes{
|
||||
Overview: true,
|
||||
DBProperties: true,
|
||||
Schema: true,
|
||||
@ -293,7 +320,7 @@ func getDiffSourceElements(cmd *cobra.Command) *diff.Elements {
|
||||
}
|
||||
}
|
||||
|
||||
return &diff.Elements{
|
||||
return &diff.Modes{
|
||||
Overview: cmdFlagIsSetTrue(cmd, flag.DiffOverview),
|
||||
DBProperties: cmdFlagIsSetTrue(cmd, flag.DiffDBProps),
|
||||
Schema: cmdFlagIsSetTrue(cmd, flag.DiffSchema),
|
||||
@ -302,24 +329,24 @@ func getDiffSourceElements(cmd *cobra.Command) *diff.Elements {
|
||||
}
|
||||
}
|
||||
|
||||
func getDiffTableElements(cmd *cobra.Command) *diff.Elements {
|
||||
func getDiffTableElements(cmd *cobra.Command) *diff.Modes {
|
||||
if !isAnyDiffElementsFlagChanged(cmd) {
|
||||
// Default
|
||||
return &diff.Elements{
|
||||
return &diff.Modes{
|
||||
Schema: true,
|
||||
RowCount: true,
|
||||
}
|
||||
}
|
||||
|
||||
if cmdFlagChanged(cmd, flag.DiffAll) {
|
||||
return &diff.Elements{
|
||||
return &diff.Modes{
|
||||
Schema: true,
|
||||
RowCount: true,
|
||||
Data: true,
|
||||
}
|
||||
}
|
||||
|
||||
return &diff.Elements{
|
||||
return &diff.Modes{
|
||||
Schema: cmdFlagIsSetTrue(cmd, flag.DiffSchema),
|
||||
RowCount: cmdFlagIsSetTrue(cmd, flag.DiffRowCount),
|
||||
Data: cmdFlagIsSetTrue(cmd, flag.DiffData),
|
||||
|
@ -125,7 +125,7 @@ func execPing(cmd *cobra.Command, args []string) error {
|
||||
err = pingSources(cmd.Context(), ru.DriverRegistry, srcs, ru.Writers.Ping, timeout)
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// It's common to cancel "sq ping". We don't want to print the cancel message.
|
||||
return errNoMsg
|
||||
return errz.ErrNoMsg
|
||||
}
|
||||
|
||||
return err
|
||||
@ -133,7 +133,7 @@ func execPing(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// pingSources pings each of the sources in srcs, and prints results
|
||||
// to w. If any error occurs pinging any of srcs, that error is printed
|
||||
// inline as part of the ping results, and an errNoMsg is returned.
|
||||
// inline as part of the ping results, and an ErrNoMsg is returned.
|
||||
//
|
||||
// NOTE: This ping code has an ancient lineage, in that it was
|
||||
// originally laid down before context.Context was a thing. Thus,
|
||||
@ -189,7 +189,7 @@ func pingSources(ctx context.Context, dp driver.Provider, srcs []*source.Source,
|
||||
// an additional error message (as the error message will already have
|
||||
// been printed by PingWriter).
|
||||
if pingErrExists {
|
||||
return errNoMsg
|
||||
return errz.ErrNoMsg
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -9,8 +9,6 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/neilotoole/sq/testh/proj"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/neilotoole/sq/cli/run"
|
||||
@ -18,6 +16,7 @@ import (
|
||||
"github.com/neilotoole/sq/libsq/core/lg/lga"
|
||||
"github.com/neilotoole/sq/libsq/core/progress"
|
||||
"github.com/neilotoole/sq/libsq/files"
|
||||
"github.com/neilotoole/sq/testh/proj"
|
||||
)
|
||||
|
||||
// newXCmd returns the "x" command, which is the container
|
||||
|
@ -27,11 +27,11 @@ func BenchmarkExecTableDiff(b *testing.B) {
|
||||
require.NoError(b, ru.Config.Collection.Add(srcA))
|
||||
require.NoError(b, ru.Config.Collection.Add(srcB))
|
||||
|
||||
elems := &diff.Elements{Data: true}
|
||||
elems := &diff.Modes{Data: true}
|
||||
cfg := &diff.Config{
|
||||
Run: ru,
|
||||
RecordWriterFn: tablew.NewRecordWriter,
|
||||
Elements: elems,
|
||||
Modes: elems,
|
||||
Lines: 3,
|
||||
Printing: output.NewPrinting(),
|
||||
Colors: diffdoc.NewColors(),
|
||||
@ -43,7 +43,7 @@ func BenchmarkExecTableDiff(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := &bytes.Buffer{}
|
||||
ru.Out = buf
|
||||
err := diff.ExecTableDiff(th.Context, cfg, srcA, sakila.TblActor, srcB, sakila.TblActor)
|
||||
_, err := diff.ExecTableDiff(th.Context, cfg, srcA, sakila.TblActor, srcB, sakila.TblActor)
|
||||
require.NoError(b, err)
|
||||
buf.Reset()
|
||||
}
|
||||
|
@ -13,12 +13,11 @@ type Config struct {
|
||||
// Run is the main program run.Run instance.
|
||||
Run *run.Run
|
||||
|
||||
// Elements specifies what elements to diff.
|
||||
Elements *Elements
|
||||
// Modes specifies what diff modes to use.
|
||||
Modes *Modes
|
||||
|
||||
// RecordWriterFn is a factory function that returns
|
||||
// an output.RecordWriter used to generate diff text
|
||||
// when comparing table data.
|
||||
// RecordWriterFn is a factory function that returns an output.RecordWriter
|
||||
// used to generate diff text when comparing table data.
|
||||
RecordWriterFn output.NewRecordWriterFunc
|
||||
|
||||
// Printing is the output.Printing instance to use when generating diff text.
|
||||
@ -39,10 +38,13 @@ type Config struct {
|
||||
// Zero indicates sequential execution; a negative values indicates unbounded
|
||||
// concurrency.
|
||||
Concurrency int
|
||||
|
||||
// StopAfter specifies the number of diffs to execute before stopping.
|
||||
StopAfter int
|
||||
}
|
||||
|
||||
// Elements determines what source elements to compare.
|
||||
type Elements struct {
|
||||
// Modes determines what diff modes to execute.
|
||||
type Modes struct {
|
||||
// Overview compares a summary of the sources.
|
||||
Overview bool
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/neilotoole/sq/cli/testrun"
|
||||
"github.com/neilotoole/sq/libsq/core/errz"
|
||||
"github.com/neilotoole/sq/libsq/source"
|
||||
"github.com/neilotoole/sq/libsq/source/drivertype"
|
||||
"github.com/neilotoole/sq/testh"
|
||||
@ -35,6 +36,7 @@ func TestSchemaDiff(t *testing.T) {
|
||||
|
||||
err := tr.Reset().Exec("diff", "@test_a", "@test_b", "--schema")
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, 1, errz.ExitCode(err), "should be exit code 1 on differences")
|
||||
fmt.Fprintln(os.Stdout, tr.Out.String())
|
||||
}
|
||||
|
@ -8,13 +8,15 @@ import (
|
||||
)
|
||||
|
||||
// ExecSourceDiff is the entrypoint to diff two sources, handle1 and handle2.
|
||||
// If differences are found, hasDiffs returns true.
|
||||
//
|
||||
// Contrast with [ExecTableDiff], which diffs two tables.
|
||||
func ExecSourceDiff(ctx context.Context, cfg *Config, src1, src2 *source.Source) error {
|
||||
elems := cfg.Elements
|
||||
func ExecSourceDiff(ctx context.Context, cfg *Config, src1, src2 *source.Source) (hasDiffs bool, err error) {
|
||||
modes := cfg.Modes
|
||||
|
||||
var differs []*diffdoc.Differ
|
||||
|
||||
if elems.Overview {
|
||||
if modes.Overview {
|
||||
doc := diffdoc.NewUnifiedDoc(diffdoc.Titlef(cfg.Colors,
|
||||
"sq diff --overview %s %s", src1.Handle, src2.Handle))
|
||||
differs = append(differs, diffdoc.NewDiffer(doc, func(ctx context.Context, _ func(error)) {
|
||||
@ -22,7 +24,7 @@ func ExecSourceDiff(ctx context.Context, cfg *Config, src1, src2 *source.Source)
|
||||
}))
|
||||
}
|
||||
|
||||
if elems.DBProperties {
|
||||
if modes.DBProperties {
|
||||
doc := diffdoc.NewUnifiedDoc(diffdoc.Titlef(cfg.Colors,
|
||||
"sq diff --dbprops %s %s", src1.Handle, src2.Handle))
|
||||
differs = append(differs, diffdoc.NewDiffer(doc, func(ctx context.Context, _ func(error)) {
|
||||
@ -30,19 +32,19 @@ func ExecSourceDiff(ctx context.Context, cfg *Config, src1, src2 *source.Source)
|
||||
}))
|
||||
}
|
||||
|
||||
if elems.Schema {
|
||||
schemaDiffers, err := differsForSchema(ctx, cfg, elems.RowCount, src1, src2)
|
||||
if modes.Schema {
|
||||
schemaDiffers, err := differsForSchema(ctx, cfg, modes.RowCount, src1, src2)
|
||||
if err != nil {
|
||||
return err
|
||||
return hasDiffs, err
|
||||
}
|
||||
differs = append(differs, schemaDiffers...)
|
||||
}
|
||||
|
||||
if elems.Data {
|
||||
if modes.Data {
|
||||
// We're going for it... diff all table data.
|
||||
dataDiffers, err := differsForAllTableData(ctx, cfg, src1, src2)
|
||||
if err != nil {
|
||||
return err
|
||||
return hasDiffs, err
|
||||
}
|
||||
differs = append(differs, dataDiffers...)
|
||||
}
|
||||
|
@ -10,13 +10,15 @@ import (
|
||||
// ExecTableDiff is the entrypoint to diff two tables, src1.table1 and
|
||||
// src2.table2.
|
||||
//
|
||||
// If differences are found, hasDiffs returns true.
|
||||
//
|
||||
// Contrast with [ExecSourceDiff], which diffs two sources.
|
||||
func ExecTableDiff(ctx context.Context, cfg *Config,
|
||||
src1 *source.Source, table1 string, src2 *source.Source, table2 string,
|
||||
) error {
|
||||
func ExecTableDiff(ctx context.Context, cfg *Config, src1 *source.Source, table1 string,
|
||||
src2 *source.Source, table2 string,
|
||||
) (hasDiffs bool, err error) {
|
||||
var (
|
||||
ru = cfg.Run
|
||||
elems = cfg.Elements
|
||||
elems = cfg.Modes
|
||||
td1 = source.Table{Handle: src1.Handle, Name: table1}
|
||||
td2 = source.Table{Handle: src2.Handle, Name: table2}
|
||||
differs []*diffdoc.Differ
|
||||
|
@ -3,6 +3,7 @@ package diff
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
@ -54,7 +55,11 @@ func differsForAllTableData(ctx context.Context, cfg *Config, src1, src2 *source
|
||||
func differForTableData(cfg *Config, title bool, td1, td2 source.Table) *diffdoc.Differ {
|
||||
var cmdTitle diffdoc.Title
|
||||
if title {
|
||||
cmdTitle = diffdoc.Titlef(cfg.Colors, "sq diff --data %s %s", td1, td2)
|
||||
if cfg.StopAfter > 0 {
|
||||
cmdTitle = diffdoc.Titlef(cfg.Colors, "sq diff --data --stop-after %d %s %s", cfg.StopAfter, td1, td2)
|
||||
} else {
|
||||
cmdTitle = diffdoc.Titlef(cfg.Colors, "sq diff --data %s %s", td1, td2)
|
||||
}
|
||||
}
|
||||
|
||||
doc := diffdoc.NewHunkDoc(
|
||||
@ -76,7 +81,7 @@ func differForTableData(cfg *Config, title bool, td1, td2 source.Table) *diffdoc
|
||||
// checked via [diffdoc.HunkDoc.Err]. Any error should also be propagated via
|
||||
// cancelFn, to cancel any peer goroutines. Note that the returned doc's
|
||||
// [diffdoc.Doc.Read] method blocks until the doc is completed (or errors out).
|
||||
func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc,
|
||||
func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc, //nolint:funlen,gocognit
|
||||
cfg *Config, td1, td2 source.Table, doc *diffdoc.HunkDoc,
|
||||
) {
|
||||
log := lg.FromContext(ctx).With(lga.Left, td1.String(), lga.Right, td2.String())
|
||||
@ -124,6 +129,18 @@ func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc,
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
log.Warn("Diff: cancelled err on errCh consumer")
|
||||
// FIXME: docs
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, errz.ErrStop) {
|
||||
log.Warn("Diff: stop error on errCh consumer")
|
||||
// FIXME: docs
|
||||
return
|
||||
}
|
||||
|
||||
log.Error("Error from record writer errCh", lga.Err, err)
|
||||
cancelFn(err)
|
||||
}
|
||||
@ -135,13 +152,22 @@ func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc,
|
||||
|
||||
qc := run.NewQueryContext(cfg.Run, nil)
|
||||
|
||||
// We give the DB query goroutines their own context, dbCtx. This is so that
|
||||
// we can explicitly stop the queries using dbCancel(errz.ErrStop) if we reach
|
||||
// the diff stop-after limit.
|
||||
dbCtx, dbCancel := context.WithCancelCause(ctx)
|
||||
go func() {
|
||||
query1 := td1.Handle + "." + stringz.DoubleQuote(td1.Name)
|
||||
// Execute DB query1; records will be sent to recw1.recCh.
|
||||
if err := libsq.ExecuteSLQ(ctx, qc, query1, recw1); err != nil {
|
||||
if errz.Has[*driver.NotExistError](err) {
|
||||
if err := libsq.ExecuteSLQ(dbCtx, qc, query1, recw1); err != nil {
|
||||
switch {
|
||||
case errz.Has[*driver.NotExistError](err):
|
||||
// For diffing, it's totally ok if a table is not found.
|
||||
log.Debug("Diff: table not found", lga.Table, td1.String())
|
||||
log.Debug("Diff: table not found", lga.Table, td2.String())
|
||||
return
|
||||
case errors.Is(err, errz.ErrStop) || errz.IsContextStop(dbCtx):
|
||||
// This means we explicitly stopped the query, probably due to reaching
|
||||
// the diff stop-after limit.
|
||||
return
|
||||
}
|
||||
|
||||
@ -161,10 +187,17 @@ func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc,
|
||||
go func() {
|
||||
query2 := td2.Handle + "." + stringz.DoubleQuote(td2.Name)
|
||||
// Execute DB query2; records will be sent to recw2.recCh.
|
||||
if err := libsq.ExecuteSLQ(ctx, qc, query2, recw2); err != nil {
|
||||
if errz.Has[*driver.NotExistError](err) {
|
||||
if err := libsq.ExecuteSLQ(dbCtx, qc, query2, recw2); err != nil {
|
||||
switch {
|
||||
case errz.Has[*driver.NotExistError](err):
|
||||
// For diffing, it's totally ok if a table is not found.
|
||||
log.Debug("Diff: table not found", lga.Table, td2.String())
|
||||
return
|
||||
case errors.Is(err, errz.ErrStop) || errz.IsContextStop(dbCtx):
|
||||
// This means we explicitly stopped the query, probably due to
|
||||
// reaching the diff stop-after limit.
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
cancelFn(err)
|
||||
@ -182,6 +215,8 @@ func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc,
|
||||
// by the diff exec goroutine further below.
|
||||
go func() {
|
||||
var rec1, rec2 record.Record
|
||||
var diffCount int
|
||||
stopAfter := cfg.StopAfter
|
||||
|
||||
for i := 0; ctx.Err() == nil; i++ {
|
||||
select {
|
||||
@ -202,7 +237,17 @@ func diffTableData(ctx context.Context, cancelFn context.CancelCauseFunc,
|
||||
return
|
||||
}
|
||||
|
||||
recPairsCh <- record.NewPair(i, rec1, rec2)
|
||||
rp := record.NewPair(i, rec1, rec2)
|
||||
if !rp.Equal() {
|
||||
diffCount++
|
||||
}
|
||||
|
||||
recPairsCh <- rp
|
||||
if stopAfter > 0 && diffCount >= stopAfter {
|
||||
dbCancel(errz.ErrStop) // Explicit stop
|
||||
close(recPairsCh)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -40,7 +40,7 @@ func PrintError(ctx context.Context, ru *run.Run, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, errNoMsg) {
|
||||
if errors.Is(err, errz.ErrNoMsg) {
|
||||
// errNoMsg is a sentinel err that sq doesn't want to print
|
||||
return
|
||||
}
|
||||
|
@ -185,6 +185,7 @@ func RegisterDefaultOpts(reg *options.Registry) {
|
||||
OptLogLevel,
|
||||
OptLogFormat,
|
||||
OptDiffNumLines,
|
||||
OptDiffStopAfter,
|
||||
OptDiffDataFormat,
|
||||
OptDiffHunkMaxSize,
|
||||
files.OptHTTPRequestTimeout,
|
||||
|
@ -16,7 +16,7 @@ func TestRegisterDefaultOpts(t *testing.T) {
|
||||
lgt.New(t).Debug("options.Registry (after)", "reg", reg)
|
||||
|
||||
keys := reg.Keys()
|
||||
require.Len(t, keys, 58)
|
||||
require.Len(t, keys, 59)
|
||||
|
||||
for _, opt := range reg.Opts() {
|
||||
opt := opt
|
||||
|
@ -50,9 +50,9 @@ func (d *Differ) execute(ctx context.Context, cancelFn func(error)) func() error
|
||||
// Zero indicates sequential execution; a negative values indicates unbounded
|
||||
// concurrency.
|
||||
//
|
||||
// The first error encountered is returned.
|
||||
func Execute(ctx context.Context, w io.Writer, concurrency int, differs []*Differ) (err error) {
|
||||
// REVISIT: should Execute accept <-chan *Differ instead of []*Differ?
|
||||
// The first error encountered is returned; hasDiff returns true if differences
|
||||
// were found, and false if no differences.
|
||||
func Execute(ctx context.Context, w io.Writer, concurrency int, differs []*Differ) (hasDiffs bool, err error) {
|
||||
defer func() {
|
||||
for _, differ := range differs {
|
||||
if differs == nil || differ.doc == nil {
|
||||
@ -90,6 +90,7 @@ func Execute(ctx context.Context, w io.Writer, concurrency int, differs []*Diffe
|
||||
rdrs = append(rdrs, differs[i].doc)
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, contextio.NewReader(ctx, io.MultiReader(rdrs...)))
|
||||
return err
|
||||
var n int64
|
||||
n, err = io.Copy(w, contextio.NewReader(ctx, io.MultiReader(rdrs...)))
|
||||
return n > 0, err
|
||||
}
|
||||
|
@ -292,6 +292,37 @@ func ExitCode(err error) (code int) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// WithExitCode returns an error that implements ExitCoder, if err is non-nil.
|
||||
// If err is nil, WithExitCode returns nil. If err already implements ExitCoder
|
||||
// and its exit code is the same as code, WithExitCode returns err unchanged.
|
||||
func WithExitCode(err error, code int) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c := ExitCode(err); c == code {
|
||||
return err
|
||||
}
|
||||
|
||||
if ez, ok := err.(*errz); ok { //nolint:errorlint
|
||||
return &exitCoder{errz: *ez, code: code}
|
||||
}
|
||||
|
||||
return &exitCoder{errz: errz{stack: callers(0), error: err}, code: code}
|
||||
}
|
||||
|
||||
var _ ExitCoder = (*exitCoder)(nil)
|
||||
|
||||
type exitCoder struct {
|
||||
errz
|
||||
code int
|
||||
}
|
||||
|
||||
// ExitCode implements ExitCoder.
|
||||
func (e *exitCoder) ExitCode() int {
|
||||
return e.code
|
||||
}
|
||||
|
||||
// Drain reads all currently available non-nil errors from errCh. If errCh is
|
||||
// nil, or there are no errors to read, Drain returns nil. If there's only a
|
||||
// single error, Drain returns it. If there are multiple errors, Drain returns
|
||||
@ -361,3 +392,29 @@ func IsErrContext(err error) bool {
|
||||
}
|
||||
return errors.Is(err, context.DeadlineExceeded)
|
||||
}
|
||||
|
||||
// ErrStop is a sentinel error a la [io.EOF] used to indicate that an explicit
|
||||
// stop condition has been reached. The stop condition is typically not an
|
||||
// indication of a failure state, but rather a signal to stop processing. It is
|
||||
// usually used in conjunction with context.CancelCauseFunc.
|
||||
//
|
||||
// See: [IsContextStop].
|
||||
var ErrStop = errors.New("explicit stop")
|
||||
|
||||
// IsContextStop returns true if ctx's cause error is [ErrStop].
|
||||
func IsContextStop(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
err := context.Cause(ctx)
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return errors.Is(err, ErrStop)
|
||||
}
|
||||
|
||||
// ErrNoMsg is a sentinel error indicating that a command has failed (and thus
|
||||
// the program should exit with a non-zero code), but no error message should be
|
||||
// printed. This is useful in the case where any error information may already
|
||||
// have been printed as part of the command output.
|
||||
var ErrNoMsg = errors.New("")
|
||||
|
@ -243,3 +243,18 @@ func TestIsErrContext(t *testing.T) {
|
||||
require.True(t, errz.IsErrContext(errz.Err(context.Canceled)))
|
||||
require.True(t, errz.IsErrContext(fmt.Errorf("wrap: %w", context.Canceled)))
|
||||
}
|
||||
|
||||
func TestWithExitCode(t *testing.T) {
|
||||
got := errz.WithExitCode(nil, 1)
|
||||
require.Nil(t, got)
|
||||
|
||||
err := errz.New("nope")
|
||||
require.Equal(t, -1, errz.ExitCode(err))
|
||||
|
||||
err = errz.WithExitCode(err, 2)
|
||||
require.Equal(t, 2, errz.ExitCode(err))
|
||||
|
||||
err = errz.WithExitCode(errz.ErrNoMsg, 3)
|
||||
require.Equal(t, 3, errz.ExitCode(err))
|
||||
require.True(t, errors.Is(err, errz.ErrNoMsg))
|
||||
}
|
||||
|
@ -264,8 +264,6 @@ func getPair[K comparable, V any](ctx context.Context, c *oncecache.Cache[K, V],
|
||||
val2, mdErr = c.Get(gCtx, key2)
|
||||
return mdErr
|
||||
})
|
||||
if err = g.Wait(); err != nil {
|
||||
return val1, val2, err
|
||||
}
|
||||
err = g.Wait()
|
||||
return val1, val2, err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user