Skip to content

Commit 5871faf

Browse files
authored
refactor(format): add Cell interface, format registry, and unified table rendering (#556)
refactor(format): add Cell interface, format registry, and unified table rendering Introduce Cell interface pattern (PlainCell, NullCell) for per-cell ANSI styling. Add format registry (RegisterFormatFunc, RegisterStreamingFormatter, RegisterValueFormatMode) for extensible format modes. Extract SQL formatter to formatsql package with self-registration via init(). Unify table rendering through TableStreamingFormatter for both streaming and buffered paths. Add format.Mode string type decoupling format package from enums.DisplayMode. Add StyledMode (AUTO/TRUE/FALSE) system variable for ANSI output control. Fix pre-existing VerticalFormatter out-of-bounds bug. Fixes #555
1 parent 0736f73 commit 5871faf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1170
-518
lines changed

enums/enums.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ const (
6565
StreamingModeFalse // Never stream
6666
)
6767

68+
// StyledMode represents the ANSI styling mode for output.
69+
//
70+
//go:generate go tool enumer -type=StyledMode -trimprefix=StyledMode -transform=snake_upper
71+
type StyledMode int
72+
73+
const (
74+
StyledModeAuto StyledMode = iota // Style if output is a TTY
75+
StyledModeTrue // Always use ANSI styling
76+
StyledModeFalse // Never use ANSI styling
77+
)
78+
6879
// IsSQLExport returns true if the display mode is one of the SQL export formats
6980
func (d DisplayMode) IsSQLExport() bool {
7081
return d == DisplayModeSQLInsert || d == DisplayModeSQLInsertOrUpdate || d == DisplayModeSQLInsertOrIgnore

enums/styledmode_enumer.go

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ require (
4040
github.com/modelcontextprotocol/go-sdk v0.3.0
4141
github.com/ngicks/go-iterator-helper v0.0.21
4242
github.com/nyaosorg/go-readline-ny v1.14.1
43-
github.com/olekukonko/tablewriter v1.0.9
43+
github.com/olekukonko/tablewriter v1.1.3
4444
github.com/samber/lo v1.51.0
4545
github.com/sourcegraph/conc v0.3.0
4646
github.com/spf13/afero v1.14.0
@@ -82,7 +82,9 @@ require (
8282
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
8383
github.com/cespare/xxhash/v2 v2.3.0 // indirect
8484
github.com/charlievieth/fastwalk v1.0.14 // indirect
85-
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
85+
github.com/clipperhouse/displaywidth v0.6.2 // indirect
86+
github.com/clipperhouse/stringish v0.1.1 // indirect
87+
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
8688
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
8789
github.com/containerd/errdefs v1.0.0 // indirect
8890
github.com/containerd/errdefs/pkg v0.3.0 // indirect
@@ -138,9 +140,9 @@ require (
138140
github.com/moby/term v0.5.2 // indirect
139141
github.com/morikuni/aec v1.0.0 // indirect
140142
github.com/nyaosorg/go-ttyadapter v0.3.0 // indirect
141-
github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect
143+
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
142144
github.com/olekukonko/errors v1.1.0 // indirect
143-
github.com/olekukonko/ll v0.1.0 // indirect
145+
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
144146
github.com/opencontainers/go-digest v1.0.0 // indirect
145147
github.com/opencontainers/image-spec v1.1.1 // indirect
146148
github.com/pascaldekloe/name v1.0.0 // indirect

go.sum

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,8 +2526,12 @@ github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86c
25262526
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
25272527
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
25282528
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
2529-
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
2530-
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
2529+
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
2530+
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
2531+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
2532+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
2533+
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
2534+
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
25312535
github.com/cloudspannerecosystem/memefish v0.6.2 h1:0R6C8KdJLLbL3aYk/rzWrwvE+bPRMqj/2MNlNvAzIPo=
25322536
github.com/cloudspannerecosystem/memefish v0.6.2/go.mod h1:mVw0xBxy0yOgm990BuR0+nqP8J+yBAAf7N/2uL69rBU=
25332537
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
@@ -2979,14 +2983,14 @@ github.com/nyaosorg/go-readline-ny v1.14.1 h1:bWyXpR6jRaCXysx4bnioxk36+YjQ6dypHK
29792983
github.com/nyaosorg/go-readline-ny v1.14.1/go.mod h1:/BDf3/H/AScnvey4LoDws1bjTZDB76EE7uKnW2apoKU=
29802984
github.com/nyaosorg/go-ttyadapter v0.3.0 h1:/Y7+rGJ0LEcs+AExevwNmND2VJvvpBmgbMuCbntKq3c=
29812985
github.com/nyaosorg/go-ttyadapter v0.3.0/go.mod h1:w6ySb/Y8rpr0uIju4vN/TMRHC/6ayabORHmEVs6d/qE=
2982-
github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 h1:ZCnkxe9GgWqqBxAk3cIKlQJuaqgOUF/nUtQs8flVTHM=
2983-
github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
2986+
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
2987+
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
29842988
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
29852989
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
2986-
github.com/olekukonko/ll v0.1.0 h1:7nX5bgpvfyxsvI90IJpOIU5zd4MBV6nRkD49e/dEx98=
2987-
github.com/olekukonko/ll v0.1.0/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo=
2988-
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
2989-
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
2990+
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
2991+
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
2992+
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
2993+
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
29902994
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
29912995
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
29922996
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=

internal/mycli/app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"github.com/samber/lo"
3636
"golang.org/x/term"
3737

38+
_ "github.com/apstndb/spanner-mycli/internal/mycli/formatsql" // Register SQL export formatters
3839
"github.com/apstndb/spanner-mycli/internal/mycli/streamio"
3940
)
4041

internal/mycli/cli_output.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func printTableData(sysVars *systemVariables, screenWidth int, out io.Writer, re
4141
if result.IsDirectOutput {
4242
for _, row := range result.Rows {
4343
if len(row) > 0 {
44-
fmt.Fprintln(out, row[0])
44+
fmt.Fprintln(out, row[0].RawText())
4545
}
4646
}
4747
return nil
@@ -75,13 +75,11 @@ func printTableData(sysVars *systemVariables, screenWidth int, out io.Writer, re
7575
// Determine the display format to use
7676
displayFormat := sysVars.Display.CLIFormat
7777

78-
// SQL export formats require values to be formatted as SQL literals for valid SQL generation.
79-
// When HasSQLFormattedValues is false, the values are formatted for display (e.g., TIMESTAMP
80-
// as "2024-01-01T00:00:00Z" instead of TIMESTAMP "2024-01-01T00:00:00Z").
81-
// Attempting to use display-formatted values in INSERT statements would generate invalid SQL.
82-
// Therefore, we fall back to table format for safety.
78+
// Modes that require SQL literal values (e.g., SQL_INSERT) must fall back to table format
79+
// when values were not formatted as SQL literals (HasSQLFormattedValues is false).
8380
// This affects metadata queries (SHOW CREATE TABLE, EXPLAIN) and DML with THEN RETURN.
84-
if sysVars.Display.CLIFormat.IsSQLExport() && !result.HasSQLFormattedValues {
81+
fmtMode := format.Mode(displayFormat.String())
82+
if format.ValueFormatModeFor(fmtMode) == format.SQLLiteralValues && !result.HasSQLFormattedValues {
8583
slog.Warn("SQL export format not applicable for this statement type, using table format instead",
8684
"requestedFormat", sysVars.Display.CLIFormat,
8785
"statementType", "non-SELECT/DML")
@@ -91,23 +89,26 @@ func printTableData(sysVars *systemVariables, screenWidth int, out io.Writer, re
9189
// Build FormatConfig from systemVariables
9290
config := sysVars.toFormatConfig()
9391

92+
// Recompute fmtMode after potential fallback above
93+
fmtMode = format.Mode(displayFormat.String())
94+
9495
// For SQL export, resolve the table name from Result if available
95-
if displayFormat.IsSQLExport() && result.SQLTableNameForExport != "" {
96+
if format.ValueFormatModeFor(fmtMode) == format.SQLLiteralValues && result.SQLTableNameForExport != "" {
9697
config.SQLTableName = result.SQLTableNameForExport
9798
}
9899

99100
// Create the appropriate formatter based on the display mode
100-
formatter, err := format.NewFormatter(displayFormat)
101+
formatter, err := format.NewFormatter(fmtMode)
101102
if err != nil {
102103
return fmt.Errorf("failed to create formatter: %w", err)
103104
}
104105

105106
// For table mode, pass verbose headers and column align via WriteTableWithParams
106-
if displayFormat == enums.DisplayModeUnspecified || displayFormat == enums.DisplayModeTable || displayFormat == enums.DisplayModeTableComment || displayFormat == enums.DisplayModeTableDetailComment {
107+
if fmtMode.IsTableMode() || fmtMode == format.ModeUnspecified {
107108
verboseHeaders := renderTableHeader(result.TableHeader, true)
108-
tableMode := displayFormat
109-
if tableMode == enums.DisplayModeUnspecified {
110-
tableMode = enums.DisplayModeTable
109+
tableMode := fmtMode
110+
if tableMode == format.ModeUnspecified {
111+
tableMode = format.ModeTable
111112
}
112113
return format.WriteTableWithParams(out, result.Rows, columnNames, config, screenWidth, tableMode, format.TableParams{
113114
VerboseHeaders: verboseHeaders,

0 commit comments

Comments
 (0)