Skip to content

Commit

Permalink
Support output for article (#127)
Browse files Browse the repository at this point in the history
* Implement CLI_FORMAT={TABLE_COMMENT|TABLE_DETAIL_COMMENT}

* Implement CLI_MARKDOWN_CODEBLOCK

* Implement CLI_ECHO_INPUT

* Add test for output

* Update README about Markdown output
  • Loading branch information
apstndb authored Feb 2, 2025
1 parent 449419f commit 1ccd595
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 31 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,42 @@ deleted rows scanned: 3 rows
optimizer version: 7
optimizer statistics: auto_20241128_05_46_13UTC
```

### Markdown output

spanner-mycli can emit input and output in Markdown.

TODO: More description

````
$ spanner-mycli -v --set 'CLI_FORMAT=TABLE_DETAIL_COMMENT' --set 'CLI_MARKDOWN_CODEBLOCK=TRUE' --set 'CLI_ECHO_INPUT=TRUE'
spanner> GRAPH FinGraph
-> MATCH (n:Account)
-> RETURN LABELS(n) AS labels, PROPERTY_NAMES(n) AS props, n.id;
```sql
GRAPH FinGraph
MATCH (n:Account)
RETURN LABELS(n) AS labels, PROPERTY_NAMES(n) AS props, n.id
/*--------------+------------------------------------------+-------+
| labels | props | id |
| ARRAY<STRING> | ARRAY<STRING> | INT64 |
+---------------+------------------------------------------+-------+
| [Account] | [create_time, id, is_blocked, nick_name] | 7 |
| [Account] | [create_time, id, is_blocked, nick_name] | 16 |
| [Account] | [create_time, id, is_blocked, nick_name] | 20 |
+---------------+------------------------------------------+-------+
3 rows in set (6.47 msecs)
timestamp: 2025-02-02T10:22:04.867235+09:00
cpu time: 4.66 msecs
rows scanned: 3 rows
deleted rows scanned: 0 rows
optimizer version: 7
optimizer statistics: auto_20250201_06_22_44UTC
*/
```
````

## How to develop

Run unit tests.
Expand Down
63 changes: 48 additions & 15 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ type DisplayMode int

const (
DisplayModeTable DisplayMode = iota
DisplayModeTableComment
DisplayModeTableDetailComment
DisplayModeVertical
DisplayModeTab
)
Expand Down Expand Up @@ -471,7 +473,7 @@ func (c *Cli) RunInteractive(ctx context.Context) int {
}
}

c.PrintResult(size, result, c.SystemVariables.CLIFormat, true)
c.PrintResult(size, result, true, input.statement)

fmt.Fprintf(c.OutStream, "\n")
cancel()
Expand Down Expand Up @@ -517,7 +519,7 @@ func (c *Cli) RunBatch(ctx context.Context, input string) int {
c.updateSystemVariables(result)
}

c.PrintResult(math.MaxInt, result, c.SystemVariables.CLIFormat, false)
c.PrintResult(math.MaxInt, result, false, "")
}

return exitCodeSuccess
Expand Down Expand Up @@ -556,7 +558,7 @@ func (c *Cli) PrintBatchError(err error) {
printError(c.ErrStream, err)
}

func (c *Cli) PrintResult(screenWidth int, result *Result, mode DisplayMode, interactive bool) {
func (c *Cli) PrintResult(screenWidth int, result *Result, interactive bool, input string) {
ostream := c.OutStream
var cmd *exec.Cmd
if c.SystemVariables.UsePager {
Expand Down Expand Up @@ -589,7 +591,7 @@ func (c *Cli) PrintResult(screenWidth int, result *Result, mode DisplayMode, int
}
}()
}
printResult(c.SystemVariables.Debug, screenWidth, ostream, result, mode, interactive, c.SystemVariables.Verbose)
printResult(c.SystemVariables, screenWidth, ostream, result, interactive, input)
}

func (c *Cli) PrintProgressingMark() func() {
Expand Down Expand Up @@ -901,15 +903,26 @@ func adjustByHeader(headers []string, availableWidth int) []int {
return adjustWidths
}

func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mode DisplayMode, interactive, verbose bool) {
func printResult(sysVars *systemVariables, screenWidth int, out io.Writer, result *Result, interactive bool, input string) {
mode := sysVars.CLIFormat

if sysVars.MarkdownCodeblock {
fmt.Fprintln(out, "```sql")
}

if sysVars.EchoInput && input != "" {
fmt.Fprintln(out, input)
}

// screenWidth <= means no limit.
if screenWidth <= 0 {
screenWidth = math.MaxInt
}

switch {
case mode == DisplayModeTable:
table := tablewriter.NewWriter(out)
switch mode {
case DisplayModeTable, DisplayModeTableComment, DisplayModeTableDetailComment:
var tableBuf strings.Builder
table := tablewriter.NewWriter(&tableBuf)
table.SetAutoFormatHeaders(false)
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAlignment(tablewriter.ALIGN_LEFT)
Expand All @@ -924,12 +937,12 @@ func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mod
slices.Values(result.ColumnTypes),
))
header := slices.Collect(xiter.Map(formatTypedHeaderColumn, slices.Values(result.ColumnTypes)))
adjustedWidths = calculateOptimalWidth(debug, screenWidth, names, slices.Concat(sliceOf(toRow(header...)), result.Rows))
adjustedWidths = calculateOptimalWidth(sysVars.Debug, screenWidth, names, slices.Concat(sliceOf(toRow(header...)), result.Rows))
} else {
adjustedWidths = calculateOptimalWidth(debug, screenWidth, result.ColumnNames, slices.Concat(sliceOf(toRow(result.ColumnNames...)), result.Rows))
adjustedWidths = calculateOptimalWidth(sysVars.Debug, screenWidth, result.ColumnNames, slices.Concat(sliceOf(toRow(result.ColumnNames...)), result.Rows))
}
var forceTableRender bool
if verbose && len(result.ColumnTypes) > 0 {
if sysVars.Verbose && len(result.ColumnTypes) > 0 {
forceTableRender = true

headers := slices.Collect(hiter.Unify(
Expand All @@ -952,7 +965,20 @@ func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mod
if forceTableRender || len(result.Rows) > 0 {
table.Render()
}
case mode == DisplayModeVertical:

s := strings.TrimSpace(tableBuf.String())
if mode == DisplayModeTableComment || mode == DisplayModeTableDetailComment {
topLeftRe := regexp.MustCompile(`^\+-`)
s = topLeftRe.ReplaceAllLiteralString(s, "/*")
}

if mode == DisplayModeTableComment {
bottomRightRe := regexp.MustCompile(`-\+$`)
s = bottomRightRe.ReplaceAllLiteralString(s, "*/")
}

fmt.Fprintln(out, s)
case DisplayModeVertical:
maxLen := 0
for _, columnName := range result.ColumnNames {
if len(columnName) > maxLen {
Expand All @@ -966,7 +992,7 @@ func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mod
fmt.Fprintf(out, format, result.ColumnNames[j], column)
}
}
case mode == DisplayModeTab:
case DisplayModeTab:
if len(result.ColumnNames) > 0 {
fmt.Fprintln(out, strings.Join(result.ColumnNames, "\t"))
for _, row := range result.Rows {
Expand All @@ -990,10 +1016,17 @@ func printResult(debug bool, screenWidth int, out io.Writer, result *Result, mod
}
fmt.Fprintln(out)
}
if verbose || result.ForceVerbose {
if sysVars.Verbose || result.ForceVerbose {
fmt.Fprint(out, resultLine(result, true))
} else if interactive {
fmt.Fprint(out, resultLine(result, verbose))
fmt.Fprint(out, resultLine(result, sysVars.Verbose))
}
if mode == DisplayModeTableDetailComment {
fmt.Fprintln(out, "*/")
}

if sysVars.MarkdownCodeblock {
fmt.Fprintln(out, "```")
}
}

Expand Down
92 changes: 76 additions & 16 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,18 @@ func TestBuildCommands(t *testing.T) {
func TestPrintResult(t *testing.T) {
t.Run("DisplayModeTable", func(t *testing.T) {
tests := []struct {
sysVars *systemVariables
desc string
displayMode DisplayMode
result *Result
screenWidth int
verbose bool
input string
want string
}{
{
desc: "DisplayModeTable: simple table",
displayMode: DisplayModeTable,
desc: "DisplayModeTable: simple table",
sysVars: &systemVariables{
CLIFormat: DisplayModeTable,
},
result: &Result{
ColumnNames: []string{"foo", "bar"},
Rows: []Row{
Expand All @@ -156,10 +158,64 @@ func TestPrintResult(t *testing.T) {
`, "\n"),
},
{
desc: "DisplayModeTable: most preceding column name",
displayMode: DisplayModeTable,
desc: "DisplayModeTableComment: simple table",
sysVars: &systemVariables{
CLIFormat: DisplayModeTableComment,
},
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"),
},
{
desc: "DisplayModeTableCommentDetail, echo, verbose, markdown",
sysVars: &systemVariables{
CLIFormat: DisplayModeTableDetailComment,
EchoInput: true,
Verbose: true,
MarkdownCodeblock: true,
},
input: "SELECT foo, bar\nFROM input",
result: &Result{
ColumnNames: []string{"foo", "bar"},
Rows: []Row{
{[]string{"1", "2"}},
{[]string{"3", "4"}},
},
IsMutation: false,
},
want: "```sql" + `
SELECT foo, bar
FROM input
/*----+-----+
| foo | bar |
+-----+-----+
| 1 | 2 |
| 3 | 4 |
+-----+-----+
Empty set
*/
` + "```\n",
},
{
desc: "DisplayModeTable: most preceding column name",
sysVars: &systemVariables{
CLIFormat: DisplayModeTable,
Verbose: true,
},
screenWidth: 20,
verbose: true,
result: &Result{
ColumnTypes: typector.MustNameCodeSlicesToStructTypeFields(
sliceOf("NAME", "LONG_NAME"),
Expand All @@ -184,10 +240,12 @@ Empty set
`, "\n"),
},
{
desc: "DisplayModeTable: also respect column type",
displayMode: DisplayModeTable,
desc: "DisplayModeTable: also respect column type",
sysVars: &systemVariables{
CLIFormat: DisplayModeTable,
Verbose: true,
},
screenWidth: 19,
verbose: true,
result: &Result{
ColumnTypes: typector.MustNameCodeSlicesToStructTypeFields(
sliceOf("NAME", "LONG_NAME"),
Expand All @@ -212,10 +270,12 @@ Empty set
`, "\n"),
},
{
desc: "DisplayModeTable: also respect column value",
displayMode: DisplayModeTable,
desc: "DisplayModeTable: also respect column value",
sysVars: &systemVariables{
CLIFormat: DisplayModeTable,
Verbose: true,
},
screenWidth: 25,
verbose: true,
result: &Result{
ColumnTypes: typector.MustNameCodeSlicesToStructTypeFields(
sliceOf("English", "Japanese"),
Expand Down Expand Up @@ -243,7 +303,7 @@ Empty set
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)
printResult(test.sysVars, test.screenWidth, out, test.result, false, test.input)

got := out.String()
if diff := cmp.Diff(test.want, got); diff != "" {
Expand All @@ -263,7 +323,7 @@ Empty set
),
IsMutation: false,
}
printResult(false, math.MaxInt, out, result, DisplayModeVertical, false, false)
printResult(&systemVariables{CLIFormat: DisplayModeVertical}, math.MaxInt, out, result, false, "")

expected := strings.TrimPrefix(`
*************************** 1. row ***************************
Expand All @@ -290,7 +350,7 @@ bar: 4
),
IsMutation: false,
}
printResult(false, math.MaxInt, out, result, DisplayModeTab, false, false)
printResult(&systemVariables{CLIFormat: DisplayModeTab}, math.MaxInt, out, result, false, "")

expected := "foo\tbar\n" +
"1\t2\n" +
Expand Down
10 changes: 10 additions & 0 deletions system_variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type systemVariables struct {
RequestTag string
UsePager bool
DatabaseDialect databasepb.DatabaseDialect
MarkdownCodeblock bool

// it is internal variable and hidden from system variable statements
ProtoDescriptor *descriptorpb.FileDescriptorSet
Expand All @@ -73,6 +74,7 @@ type systemVariables struct {
AutoWrap bool
EchoExecutedDDL bool
EnableHighlight bool
EchoInput bool

// TODO: Expose as CLI_*
EnableProgressBar bool
Expand Down Expand Up @@ -325,6 +327,10 @@ var accessorMap = map[string]accessor{
switch strings.ToUpper(unquoteString(value)) {
case "TABLE":
this.CLIFormat = DisplayModeTable
case "TABLE_COMMENT":
this.CLIFormat = DisplayModeTableComment
case "TABLE_DETAIL_COMMENT":
this.CLIFormat = DisplayModeTableDetailComment
case "VERTICAL":
this.CLIFormat = DisplayModeVertical
case "TAB":
Expand Down Expand Up @@ -383,6 +389,7 @@ var accessorMap = map[string]accessor{
"CLI_ROLE": {
Getter: stringGetter(func(sysVars *systemVariables) *string { return &sysVars.Role }),
},
"CLI_ECHO_INPUT": boolAccessor(func(sysVars *systemVariables) *bool { return &sysVars.EchoInput }),
"CLI_ENDPOINT": {
Getter: stringGetter(func(sysVars *systemVariables) *string { return &sysVars.Endpoint }),
},
Expand Down Expand Up @@ -510,6 +517,9 @@ var accessorMap = map[string]accessor{
"CLI_PROTOTEXT_MULTILINE": boolAccessor(func(variables *systemVariables) *bool {
return &variables.MultilineProtoText
}),
"CLI_MARKDOWN_CODEBLOCK": boolAccessor(func(variables *systemVariables) *bool {
return &variables.MarkdownCodeblock
}),
"CLI_QUERY_MODE": {
Getter: func(this *systemVariables, name string) (map[string]string, error) {
if this.QueryMode == nil {
Expand Down

0 comments on commit 1ccd595

Please sign in to comment.