diff --git a/README.md b/README.md index 5440405..9e2d500 100644 --- a/README.md +++ b/README.md @@ -231,47 +231,47 @@ and `{}` for a mutually exclusive keyword. * The syntax is case-insensitive. * `\G` delimiter is also supported for displaying results vertically. -| Usage | Syntax | Note | -| --- | --- | --- | -| List databases | `SHOW DATABASES;` | | -| Switch database | `USE [ROLE ];` | The role you set is used for accessing with [fine-grained access control](https://cloud.google.com/spanner/docs/fgac-about). | -| Create database | `CREATE DATABSE ;` | | -| Drop database | `DROP DATABASE ;` | | -| List tables | `SHOW TABLES [];` | If schema is not provided, default schema is used | -| Show table schema | `SHOW CREATE TABLE ;` | The table can be a FQN.| -| Show columns | `SHOW COLUMNS FROM
;` | The table can be a FQN.| -| Show indexes | `SHOW INDEX FROM
;` | The table can be a FQN.| -| Create table | `CREATE TABLE ...;` | | -| Change table schema | `ALTER TABLE ...;` | | -| Delete table | `DROP TABLE ...;` | | -| Truncate table | `TRUNCATE TABLE
;` | Only rows are deleted. Note: Non-atomically because executed as a [partitioned DML statement](https://cloud.google.com/spanner/docs/dml-partitioned?hl=en). | -| Create index | `CREATE INDEX ...;` | | -| Delete index | `DROP INDEX ...;` | | -| Create role | `CREATE ROLE ...;` | | -| Drop role | `DROP ROLE ...;` | | -| Grant | `GRANT ...;` | | -| Revoke | `REVOKE ...;` | | -| Query | `SELECT ...;` | | -| DML | `{INSERT\|UPDATE\|DELETE} ...;` | | -| Partitioned DML | `PARTITIONED {UPDATE\|DELETE} ...;` | | -| Show local proto descriptors | `SHOW LOCAL PROTO;` | | -| Show remote proto bundle | `SHOW PROTO;` | | -| Show Query Execution Plan | `EXPLAIN SELECT ...;` | | -| Show DML Execution Plan | `EXPLAIN {INSERT\|UPDATE\|DELETE} ...;` | | -| Show Query Execution Plan with Stats | `EXPLAIN ANALYZE SELECT ...;` | | -| Show DML Execution Plan with Stats | `EXPLAIN ANALYZE {INSERT\|UPDATE\|DELETE} ...;` | | -| Show Query Result Shape | `DESCRIBE SELECT ...;` | | -| Show DML Result Shape | `DESCRIBE {INSERT\|UPDATE\|DELETE} ... THEN RETURN ...;` | | -| Start a new query optimizer statistics package construction | `ANALYZE;` | | -| Start Read-Write Transaction | `BEGIN [RW] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG ];` | See [Request Priority](#request-priority) for details on the priority. The tag you set is used as both transaction tag and request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).| -| Commit Read-Write Transaction | `COMMIT;` | | -| Rollback Read-Write Transaction | `ROLLBACK;` | | +| Usage | Syntax | Note | +| --- |------------------------------------------------------------------------------------------------| --- | +| List databases | `SHOW DATABASES;` | | +| Switch database | `USE [ROLE ];` | The role you set is used for accessing with [fine-grained access control](https://cloud.google.com/spanner/docs/fgac-about). | +| Create database | `CREATE DATABSE ;` | | +| Drop database | `DROP DATABASE ;` | | +| List tables | `SHOW TABLES [];` | If schema is not provided, default schema is used | +| Show table schema | `SHOW CREATE TABLE
;` | The table can be a FQN.| +| Show columns | `SHOW COLUMNS FROM
;` | The table can be a FQN.| +| Show indexes | `SHOW INDEX FROM
;` | The table can be a FQN.| +| Create table | `CREATE TABLE ...;` | | +| Change table schema | `ALTER TABLE ...;` | | +| Delete table | `DROP TABLE ...;` | | +| Truncate table | `TRUNCATE TABLE
;` | Only rows are deleted. Note: Non-atomically because executed as a [partitioned DML statement](https://cloud.google.com/spanner/docs/dml-partitioned?hl=en). | +| Create index | `CREATE INDEX ...;` | | +| Delete index | `DROP INDEX ...;` | | +| Create role | `CREATE ROLE ...;` | | +| Drop role | `DROP ROLE ...;` | | +| Grant | `GRANT ...;` | | +| Revoke | `REVOKE ...;` | | +| Query | `SELECT ...;` | | +| DML | `{INSERT\|UPDATE\|DELETE} ...;` | | +| Partitioned DML | `PARTITIONED {UPDATE\|DELETE} ...;` | | +| Show local proto descriptors | `SHOW LOCAL PROTO;` | | +| Show remote proto bundle | `SHOW REMOTE PROTO;` | | +| Show Query Execution Plan | `EXPLAIN SELECT ...;` | | +| Show DML Execution Plan | `EXPLAIN {INSERT\|UPDATE\|DELETE} ...;` | | +| Show Query Execution Plan with Stats | `EXPLAIN ANALYZE SELECT ...;` | | +| Show DML Execution Plan with Stats | `EXPLAIN ANALYZE {INSERT\|UPDATE\|DELETE} ...;` | | +| Show Query Result Shape | `DESCRIBE SELECT ...;` | | +| Show DML Result Shape | `DESCRIBE {INSERT\|UPDATE\|DELETE} ... THEN RETURN ...;` | | +| Start a new query optimizer statistics package construction | `ANALYZE;` | | +| Start Read-Write Transaction | `BEGIN [RW] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG ];` | See [Request Priority](#request-priority) for details on the priority. The tag you set is used as both transaction tag and request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).| +| Commit Read-Write Transaction | `COMMIT;` | | +| Rollback Read-Write Transaction | `ROLLBACK;` | | | Start Read-Only Transaction | `BEGIN RO [{\|}] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG ];` | `` and `` is used for stale read. See [Request Priority](#request-priority) for details on the priority. The tag you set is used as request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).| -| End Read-Only Transaction | `CLOSE;` | | -| Exit CLI | `EXIT;` | | -| Show variable | `SHOW VARIABLE ;` | | -| Set variable | `SET = ;` | | -| Show variables | `SHOW VARIABLES;` | | +| End Read-Only Transaction | `CLOSE;` | | +| Exit CLI | `EXIT;` | | +| Show variable | `SHOW VARIABLE ;` | | +| Set variable | `SET = ;` | | +| Show variables | `SHOW VARIABLES;` | | ## System Variables diff --git a/cli.go b/cli.go index e5af37f..077fcf7 100644 --- a/cli.go +++ b/cli.go @@ -33,12 +33,13 @@ import ( "strings" "time" + "github.com/mattn/go-runewidth" + "golang.org/x/term" "github.com/apstndb/lox" "github.com/ngicks/go-iterator-helper/x/exp/xiter" - "github.com/chzyer/readline/runes" "github.com/ngicks/go-iterator-helper/hiter" "github.com/reeflective/readline/inputrc" "github.com/samber/lo" @@ -436,59 +437,12 @@ func readInteractiveInput(rl *readline.Shell, prompt string) (*inputStatement, e } -func splitLineWithWidth(s string, maxWidth int) iter.Seq[string] { - return func(yield func(string) bool) { - for line := range hiter.StringsSplitFunc(s, 0, hiter.StringsCutNewLine) { - lineRunes := []rune(line) - if maxWidth >= runes.WidthAll(lineRunes) { - if !yield(line) { - return - } - continue - } - - var sb strings.Builder - currentWidth := 0 - for _, r := range lineRunes { - runeWidth := runes.Width(r) - if currentWidth+runeWidth > maxWidth { - if !yield(sb.String()) { - return - } - sb.Reset() - currentWidth = 0 - } - - sb.WriteRune(r) - currentWidth += runeWidth - } - - if sb.Len() > 0 { - yield(sb.String()) - } - } - } -} - -func WrapLines(width int, s string) string { - return strings.Join( - slices.Collect( - splitLineWithWidth(s, width), - ), - "\n") -} - func maxWidth(s string) int { return hiter.Max(xiter.Map( - func(in string) int { - return runes.WidthAll([]rune(in)) - }, + runewidth.StringWidth, hiter.StringsSplitFunc(s, 0, hiter.StringsCutNewLine))) } -func stringWidthAll(s string) int { - return runes.WidthAll([]rune(s)) -} func clipToMax[S interface{ ~[]E }, E cmp.Ordered](s S, maxValue E) iter.Seq[E] { return xiter.Map( func(in E) E { @@ -497,6 +451,7 @@ func clipToMax[S interface{ ~[]E }, E cmp.Ordered](s S, maxValue E) iter.Seq[E] slices.Values(s), ) } + func adjustToSum(limit int, vs []int) ([]int, int) { sumVs := lo.Sum(vs) remains := limit - sumVs @@ -504,7 +459,6 @@ func adjustToSum(limit int, vs []int) ([]int, int) { return vs, remains } - // maxV := slices.Max(vs) curVs := vs for i := 1; ; i++ { rev := lo.Reverse(slices.Sorted(slices.Values(lo.Uniq(vs)))) @@ -520,18 +474,22 @@ func adjustToSum(limit int, vs []int) ([]int, int) { return curVs, limit - lo.Sum(curVs) } +var invalidWidthCount = WidthCount{ + // impossible to fit any width + width: math.MaxInt, + // least significant + count: math.MinInt, +} + func maxIndex(ignoreMax int, adjustWidths []int, seq iter.Seq[WidthCount]) (int, WidthCount) { - current := -1 - maxIdx := -1 - var candidate WidthCount - for v := range seq { - current++ - if ignoreMax >= v.Length-adjustWidths[current] && v.Count > candidate.Count { - candidate = v - maxIdx = current - } - } - return maxIdx, candidate + return MaxByWithIdx( + invalidWidthCount, + WidthCount.Count, + hiter.Unify( + func(adjustWidth int, wc WidthCount) WidthCount { + return lo.Ternary(wc.Length()-adjustWidth <= ignoreMax, wc, invalidWidthCount) + }, + hiter.Pairs(slices.Values(adjustWidths), seq))) } func calculateOptimalWidth(debug bool, screenWidth int, types []*sppb.StructType_Field, rows []Row) []int { @@ -541,20 +499,20 @@ func calculateOptimalWidth(debug bool, screenWidth int, types []*sppb.StructType overheadWidth := 4 + 3*(len(types)-1) // don't mutate - remainsWidth := screenWidth - overheadWidth + termWidthWithoutOverhead := screenWidth - overheadWidth if debug { - log.Printf("screenWitdh: %v, remainsWidth: %v", screenWidth, remainsWidth) + log.Printf("screenWitdh: %v, remainsWidth: %v", screenWidth, termWidthWithoutOverhead) } formatIntermediate := func(remainsWidth int, adjustedWidths []int) string { return fmt.Sprintf("remaining %v, adjustedWidths: %v", remainsWidth-lo.Sum(adjustedWidths), adjustedWidths) } - adjustWidths := adjustByName(types, remainsWidth) + adjustedWidths := adjustByName(types, termWidthWithoutOverhead) if debug { - log.Println("adjustByName:", formatIntermediate(remainsWidth, adjustWidths)) + log.Println("adjustByName:", formatIntermediate(termWidthWithoutOverhead, adjustedWidths)) } var transposedRows [][]string @@ -570,7 +528,7 @@ func calculateOptimalWidth(debug bool, screenWidth int, types []*sppb.StructType )))) } - widthCounts := calculateWidthCounts(adjustWidths, transposedRows) + widthCounts := calculateWidthCounts(adjustedWidths, transposedRows) for { if debug { log.Println("widthCounts:", widthCounts) @@ -578,57 +536,62 @@ func calculateOptimalWidth(debug bool, screenWidth int, types []*sppb.StructType firstCounts := xiter.Map( - func(in []WidthCount) WidthCount { - return lo.FirstOr(in, WidthCount{ - Length: math.MinInt, - Count: 0, - }) + func(wcs []WidthCount) WidthCount { + return lo.FirstOr(wcs, invalidWidthCount) }, slices.Values(widthCounts)) - idx, target := maxIndex(remainsWidth-lo.Sum(adjustWidths), adjustWidths, firstCounts) - if idx < 0 { + // find the largest count idx within available width + idx, target := maxIndex(termWidthWithoutOverhead-lo.Sum(adjustedWidths), adjustedWidths, firstCounts) + if idx < 0 || target.Count() < 1 { break } widthCounts[idx] = widthCounts[idx][1:] - adjustWidths[idx] = target.Length + adjustedWidths[idx] = target.Length() if debug { - log.Println("adjusting:", formatIntermediate(remainsWidth, adjustWidths)) + log.Println("adjusting:", formatIntermediate(termWidthWithoutOverhead, adjustedWidths)) } } if debug { - log.Println("semi final:", formatIntermediate(remainsWidth, adjustWidths)) + log.Println("semi final:", formatIntermediate(termWidthWithoutOverhead, adjustedWidths)) } + // Add rest to the longest shortage column. longestWidths := lo.Map(widthCounts, func(item []WidthCount, index int) int { - return hiter.Max(xiter.Map(func(wc WidthCount) int { return wc.Length }, slices.Values(item))) + return hiter.Max(xiter.Map(WidthCount.Length, slices.Values(item))) }) - idx, _ := MaxByWithIdx(math.MinInt, hiter.Unify(func(first, second int) int { - return second - first - }, hiter.Pairs(slices.Values(adjustWidths), slices.Values(longestWidths)))) + idx, _ := MaxWithIdx(math.MinInt, hiter.Unify( + func(longestWidth, adjustedWidth int) int { + return longestWidth - adjustedWidth + }, + hiter.Pairs(slices.Values(longestWidths), slices.Values(adjustedWidths)))) if idx != -1 { - adjustWidths[idx] += remainsWidth - lo.Sum(adjustWidths) + adjustedWidths[idx] += termWidthWithoutOverhead - lo.Sum(adjustedWidths) } if debug { - log.Println("final:", formatIntermediate(remainsWidth, adjustWidths)) + log.Println("final:", formatIntermediate(termWidthWithoutOverhead, adjustedWidths)) } - return adjustWidths + return adjustedWidths } -func MaxByWithIdx[E cmp.Ordered](fallback E, seq iter.Seq[E]) (int, E) { +func MaxWithIdx[E cmp.Ordered](fallback E, seq iter.Seq[E]) (int, E) { + return MaxByWithIdx(fallback, lox.Identity, seq) +} + +func MaxByWithIdx[O cmp.Ordered, E any](fallback E, f func(E) O, seq iter.Seq[E]) (int, E) { val := fallback idx := -1 current := -1 for v := range seq { current++ - if val < v { + if f(val) < f(v) { val = v idx = current } @@ -636,13 +599,15 @@ func MaxByWithIdx[E cmp.Ordered](fallback E, seq iter.Seq[E]) (int, E) { return idx, val } -func countLen(ss []string) iter.Seq[WidthCount] { - return xiter.Map(func(in lo.Entry[int, int]) WidthCount { - return WidthCount{ - Length: in.Key, - Count: in.Value, - } - }, slices.Values(lox.EntriesSortedByKey(lo.CountValuesBy(ss, maxWidth)))) +func countWidth(ss []string) iter.Seq[WidthCount] { + return xiter.Map( + func(e lo.Entry[int, int]) WidthCount { + return WidthCount{ + width: e.Key, + count: e.Value, + } + }, + slices.Values(lox.EntriesSortedByKey(lo.CountValuesBy(ss, maxWidth)))) } func calculateWidthCounts(currentWidths []int, rows [][]string) [][]WidthCount { @@ -653,23 +618,26 @@ func calculateWidthCounts(currentWidths []int, rows [][]string) [][]WidthCount { largerWidthCounts := slices.Collect( xiter.Filter( func(v WidthCount) bool { - return v.Length > currentWidth + return v.Length() > currentWidth }, - countLen(columnValues), + countWidth(columnValues), )) result = append(result, largerWidthCounts) } return result } -type WidthCount struct{ Length, Count int } +type WidthCount struct{ width, count int } + +func (wc WidthCount) Length() int { return wc.width } +func (wc WidthCount) Count() int { return wc.count } func adjustByName(types []*sppb.StructType_Field, availableWidth int) []int { names := slices.Collect(xiter.Map( (*sppb.StructType_Field).GetName, slices.Values(types), )) - nameWidths := slices.Collect(xiter.Map(stringWidthAll, slices.Values(names))) + nameWidths := slices.Collect(xiter.Map(runewidth.StringWidth, slices.Values(names))) adjustWidths, _ := adjustToSum(availableWidth, nameWidths) @@ -677,6 +645,11 @@ func adjustByName(types []*sppb.StructType_Field, availableWidth int) []int { } func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mode DisplayMode, interactive, verbose bool) { + // screenWidth <= means no limit. + if screenWidth <= 0 { + screenWidth = math.MaxInt + } + if mode == DisplayModeTable { table := tablewriter.NewWriter(out) table.SetAutoFormatHeaders(false) @@ -684,17 +657,20 @@ func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mod table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetAutoWrapText(false) + // It is not valid when ColumnType is not populated. adjustedWidths := calculateOptimalWidth(debug, screenWidth, result.ColumnTypes, result.Rows) - var forceTableRender bool // This condition is true if statement is SelectStatement or DmlStatement - + var forceTableRender bool if verbose && len(result.ColumnTypes) > 0 { forceTableRender = true - var headers []string - for i, field := range result.ColumnTypes { - headers = append(headers, WrapLines(adjustedWidths[i], formatTypedHeaderColumn(field))) - } + + headers := slices.Collect(hiter.Unify( + runewidth.Wrap, + hiter.Pairs( + xiter.Map(formatTypedHeaderColumn, slices.Values(result.ColumnTypes)), + slices.Values(adjustedWidths))), + ) table.SetHeader(headers) } else { table.SetHeader(result.ColumnNames) @@ -703,10 +679,8 @@ func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mod for _, row := range result.Rows { if len(result.ColumnTypes) > 0 { wrappedColumns := slices.Collect(hiter.Unify( - func(header int, col string) string { - return WrapLines(header, col) - }, - hiter.Pairs(slices.Values(adjustedWidths), slices.Values(row.Columns))), + runewidth.Wrap, + hiter.Pairs(slices.Values(row.Columns), slices.Values(adjustedWidths))), ) table.Append(wrappedColumns) } else { diff --git a/cli_test.go b/cli_test.go index c83721f..e306ab8 100644 --- a/cli_test.go +++ b/cli_test.go @@ -127,29 +127,129 @@ func TestBuildCommands(t *testing.T) { func TestPrintResult(t *testing.T) { t.Run("DisplayModeTable", func(t *testing.T) { - out := &bytes.Buffer{} - result := &Result{ - ColumnNames: []string{"foo", "bar"}, - Rows: []Row{ - Row{[]string{"1", "2"}}, - Row{[]string{"3", "4"}}, - }, - IsMutation: false, - } - printResult(false, math.MaxInt, out, result, DisplayModeTable, false, false) - - expected := strings.TrimPrefix(` + tests := []struct { + desc string + displayMode DisplayMode + result *Result + screenWidth int + verbose bool + want string + }{ + { + desc: "DisplayModeTable: simple table", + displayMode: DisplayModeTable, + result: &Result{ + ColumnNames: []string{"foo", "bar"}, + Rows: []Row{ + {[]string{"1", "2"}}, + {[]string{"3", "4"}}, + }, + IsMutation: false, + }, + want: strings.TrimPrefix(` +-----+-----+ | foo | bar | +-----+-----+ | 1 | 2 | | 3 | 4 | +-----+-----+ -`, "\n") +`, "\n"), + }, + { + desc: "DisplayModeTable: most preceding column name", + displayMode: DisplayModeTable, + screenWidth: 20, + verbose: true, + result: &Result{ + ColumnTypes: []*sppb.StructType_Field{ + {Name: "NAME", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + {Name: "LONG_NAME", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + }, + Rows: []Row{ + {[]string{"1", "2"}}, + {[]string{"3", "4"}}, + }, + IsMutation: false, + }, + want: strings.TrimPrefix(` ++------+-----------+ +| NAME | LONG_NAME | +| STRI | STRING | +| NG | | ++------+-----------+ +| 1 | 2 | +| 3 | 4 | ++------+-----------+ +Empty set () +`, "\n"), + }, + { + desc: "DisplayModeTable: also respect column type", + displayMode: DisplayModeTable, + screenWidth: 19, + verbose: true, + result: &Result{ + ColumnTypes: []*sppb.StructType_Field{ + {Name: "NAME", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + {Name: "LONG_NAME", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + }, + Rows: []Row{ + {[]string{"1", "2"}}, + {[]string{"3", "4"}}, + }, + IsMutation: false, + }, + want: strings.TrimPrefix(` ++--------+--------+ +| NAME | LONG_N | +| STRING | AME | +| | STRING | ++--------+--------+ +| 1 | 2 | +| 3 | 4 | ++--------+--------+ +Empty set () +`, "\n"), + }, + { + desc: "DisplayModeTable: also respect column value", + displayMode: DisplayModeTable, + screenWidth: 25, + verbose: true, + result: &Result{ + ColumnTypes: []*sppb.StructType_Field{ + {Name: "English", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + {Name: "Japanese", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + }, + Rows: []Row{ + {[]string{"Hello World", "こんにちは"}}, + {[]string{"Bye", "さようなら"}}, + }, + IsMutation: false, + }, + want: strings.TrimPrefix(` ++----------+------------+ +| English | Japanese | +| STRING | STRING | ++----------+------------+ +| Hello Wo | こんにちは | +| rld | | +| Bye | さようなら | ++----------+------------+ +Empty set () +`, "\n"), + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + out := &bytes.Buffer{} + printResult(false, test.screenWidth, out, test.result, test.displayMode, false, test.verbose) - got := out.String() - if got != expected { - t.Errorf("invalid print: expected = %s, but got = %s", expected, got) + got := out.String() + if diff := cmp.Diff(test.want, got); diff != "" { + t.Errorf("result differ: %v", diff) + } + }) } }) diff --git a/emulator.go b/emulator.go index 3b1b9f2..8d636a5 100644 --- a/emulator.go +++ b/emulator.go @@ -5,6 +5,8 @@ import ( "fmt" "log" + "github.com/testcontainers/testcontainers-go" + database "cloud.google.com/go/spanner/admin/database/apiv1" "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" instance "cloud.google.com/go/spanner/admin/instance/apiv1" @@ -17,8 +19,16 @@ import ( "github.com/testcontainers/testcontainers-go/modules/gcloud" ) +type noopLogger struct{} + +// Printf implements testcontainers.Logging. +func (n noopLogger) Printf(string, ...interface{}) { +} + func newEmulator(ctx context.Context, opts spannerOptions) (container *gcloud.GCloudContainer, teardown func(), err error) { - container, err = gcloud.RunSpanner(ctx, lo.CoalesceOrEmpty(opts.EmulatorImage, defaultEmulatorImage)) + // Workaround to suppress log output with `-v`. + testcontainers.Logger = &noopLogger{} + container, err = gcloud.RunSpanner(ctx, lo.CoalesceOrEmpty(opts.EmulatorImage, defaultEmulatorImage), testcontainers.WithLogger(&noopLogger{})) if err != nil { return nil, nil, err } diff --git a/go.mod b/go.mod index b23fcab..a3b3c75 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( github.com/apstndb/lox v0.0.0-20241102092239-40172f618f5c github.com/apstndb/spantype v0.2.0 github.com/apstndb/spanvalue v0.0.0-20241103175520-dc3408b8d84e - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/cloudspannerecosystem/memefish v0.0.0-20241031032728-970586f04071 github.com/google/go-cmp v0.6.0 github.com/jessevdk/go-flags v1.6.1 + github.com/mattn/go-runewidth v0.0.10 github.com/ngicks/go-iterator-helper v0.0.15 github.com/olekukonko/tablewriter v0.0.5 github.com/reeflective/readline v1.0.15 @@ -72,7 +72,6 @@ require ( github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-runewidth v0.0.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect diff --git a/go.sum b/go.sum index 61ae43f..3177dbc 100644 --- a/go.sum +++ b/go.sum @@ -674,7 +674,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=