Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli/sql: properly format common array types #66079

Merged
merged 1 commit into from
Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ func Example_sql_format() {
c.RunWithArgs([]string{"sql", "-e",
"select '2021-03-20'::date; select '01:01'::time; select '01:01'::timetz; select '01:01+02:02'::timetz"})
c.RunWithArgs([]string{"sql", "-e", "select (1/3.0)::real; select (1/3.0)::double precision; select '-inf'::float8"})
// Special characters inside arrays used to be represented as escaped bytes.
c.RunWithArgs([]string{"sql", "-e", "select array['哈哈'::TEXT], array['哈哈'::NAME], array['哈哈'::VARCHAR]"})
c.RunWithArgs([]string{"sql", "-e", "select array['哈哈'::CHAR(2)], array['哈'::\"char\"]"})
// Preserve quoting of arrays containing commas or double quotes.
c.RunWithArgs([]string{"sql", "-e", `select array['a,b', 'a"b', 'a\b']`, "--format=table"})
// Infinities inside float arrays used to be represented differently from infinities as simpler scalar.
c.RunWithArgs([]string{"sql", "-e", "select array['Inf'::FLOAT4, '-Inf'::FLOAT4], array['Inf'::FLOAT8]"})
// Sanity check for other array types.
c.RunWithArgs([]string{"sql", "-e", "select array[true, false], array['01:01'::time], array['2021-03-20'::date]"})
c.RunWithArgs([]string{"sql", "-e", "select array[123::int2], array[123::int4], array[123::int8]"})

// Output:
// sql -e create database t; create table t.times (bare timestamp, withtz timestamptz)
Expand Down Expand Up @@ -321,6 +331,26 @@ func Example_sql_format() {
// 0.3333333333333333
// float8
// -Infinity
// sql -e select array['哈哈'::TEXT], array['哈哈'::NAME], array['哈哈'::VARCHAR]
// array array array
// {哈哈} {哈哈} {哈哈}
// sql -e select array['哈哈'::CHAR(2)], array['哈'::"char"]
// array array
// {哈哈} {哈}
// sql -e select array['a,b', 'a"b', 'a\b'] --format=table
// array
// -------------------------
// {"a,b","a\"b","a\\b"}
// (1 row)
// sql -e select array['Inf'::FLOAT4, '-Inf'::FLOAT4], array['Inf'::FLOAT8]
// array array
// {Infinity,-Infinity} {Infinity}
// sql -e select array[true, false], array['01:01'::time], array['2021-03-20'::date]
// array array array
// {true,false} {01:01:00} {2021-03-20}
// sql -e select array[123::int2], array[123::int4], array[123::int8]
// array array array
// {123} {123} {123}
}

func Example_sql_column_labels() {
Expand Down
92 changes: 89 additions & 3 deletions pkg/cli/sql_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ package cli

import (
"context"
gosql "database/sql"
"database/sql/driver"
"fmt"
"io"
"math"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -1148,9 +1150,17 @@ func isNotGraphicUnicodeOrTabOrNewline(r rune) bool {
func formatVal(
val driver.Value, colType string, showPrintableUnicode bool, showNewLinesAndTabs bool,
) string {
if b, ok := val.([]byte); ok && colType == "NAME" {
val = string(b)
colType = "VARCHAR"
log.VInfof(context.Background(), 2, "value: go %T, sql %q", val, colType)

if b, ok := val.([]byte); ok {
if strings.HasPrefix(colType, "_") && len(b) > 0 && b[0] == '{' {
return formatArray(b, colType[1:], showPrintableUnicode, showNewLinesAndTabs)
}

if colType == "NAME" {
val = string(b)
colType = "VARCHAR"
}
}

switch t := val.(type) {
Expand Down Expand Up @@ -1224,6 +1234,82 @@ func formatVal(
return fmt.Sprint(val)
}

func formatArray(
b []byte, colType string, showPrintableUnicode bool, showNewLinesAndTabs bool,
) string {
// backingArray is the array we're going to parse the server data
// into.
var backingArray interface{}
// parsingArray is a helper structure provided by lib/pq to parse
// arrays.
var parsingArray gosql.Scanner

// lib.pq has different array parsers for special value types.
//
// TODO(knz): This would better use a general-purpose parser
// using the OID to look up an array parser in crdb's sql package.
// However, unfortunately the OID is hidden from us.
switch colType {
case "BOOL":
boolArray := []bool{}
backingArray = &boolArray
parsingArray = (*pq.BoolArray)(&boolArray)
case "FLOAT4", "FLOAT8":
floatArray := []float64{}
backingArray = &floatArray
parsingArray = (*pq.Float64Array)(&floatArray)
case "INT2", "INT4", "INT8", "OID":
intArray := []int64{}
backingArray = &intArray
parsingArray = (*pq.Int64Array)(&intArray)
case "TEXT", "VARCHAR", "NAME", "CHAR", "BPCHAR":
stringArray := []string{}
backingArray = &stringArray
parsingArray = (*pq.StringArray)(&stringArray)
default:
genArray := [][]byte{}
backingArray = &genArray
parsingArray = &pq.GenericArray{A: &genArray}
}

// Now ask the pq array parser to convert the byte slice
// from the server into a Go array.
if err := parsingArray.Scan(b); err != nil {
// A parsing failure is not a catastrophe; we can still print out
// the array as a byte slice. This will do in many cases.
log.VInfof(context.Background(), 1, "unable to parse %q (sql %q) as array: %v", b, colType, err)
return formatVal(b, "BYTEA", showPrintableUnicode, showNewLinesAndTabs)
}

// We have a go array in "backingArray". Now print it out.
var buf strings.Builder
buf.WriteByte('{')
comma := "" // delimiter
v := reflect.ValueOf(backingArray).Elem()
for i := 0; i < v.Len(); i++ {
buf.WriteString(comma)

// Access the i-th element in the backingArray.
arrayVal := driver.Value(v.Index(i).Interface())
// Format the value recursively into a string.
vs := formatVal(arrayVal, colType, showPrintableUnicode, showNewLinesAndTabs)

// If the value contains special characters or a comma, enclose in double quotes.
// Also escape the special characters.
if strings.IndexByte(vs, ',') >= 0 || reArrayStringEscape.MatchString(vs) {
vs = "\"" + reArrayStringEscape.ReplaceAllString(vs, "\\$1") + "\""
}

// Add the string for that one value to the output array representation.
buf.WriteString(vs)
comma = ","
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the strings inside of a string array already correctly escaped if they have commas in them? i.e., the array element should then be surrounded with double quotes

}
buf.WriteByte('}')
return buf.String()
}

var reArrayStringEscape = regexp.MustCompile(`(["\\])`)

var timeOutputFormats = map[string]string{
"TIMESTAMP": timeutil.TimestampWithoutTZFormat,
"TIMESTAMPTZ": timeutil.TimestampWithTZFormat,
Expand Down