From 2a21e5cff1e3a80efdc9d0f4a05415511961e76e Mon Sep 17 00:00:00 2001 From: Rafi Shamim Date: Tue, 11 May 2021 23:55:40 -0400 Subject: [PATCH 1/3] pgwire: only show TZ minutes offset if non-zero Similar to #57265, this commit matches the PG behavior to only show the minutes offset if it is non-zero. Release note (bug fix): Minute timezone offsets are only displayed in the wire protocol if they are non-zero for TimestampTZ and TimeTZ values. Previously, they would always display. Release note (bug fix): Binary TimeTZ values were not being decoded correctly when being sent as a parameter in the wire protocol. This is now fixed. --- pkg/cmd/generate-binary/main.go | 12 +- pkg/sql/pgwire/testdata/encodings.json | 154 ++++++++++++++++++++++++ pkg/sql/pgwire/testdata/pgtest/timezone | 7 +- pkg/sql/pgwire/types.go | 12 +- 4 files changed, 174 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/generate-binary/main.go b/pkg/cmd/generate-binary/main.go index bf1aa530ff91..16a5d559c285 100644 --- a/pkg/cmd/generate-binary/main.go +++ b/pkg/cmd/generate-binary/main.go @@ -297,7 +297,6 @@ var inputs = map[string][]string{ "9004-10-19 10:23:54", }, - /* TODO(mjibson): fix these; there's a slight timezone display difference "'%s'::timestamptz": { "1999-01-08 04:05:06+00", "1999-01-08 04:05:06+00:00", @@ -312,10 +311,19 @@ var inputs = map[string][]string{ "4004-10-19 10:23:54", "9004-10-19 10:23:54", }, - */ "'%s'::timetz": { + "04:05:06+00", + "04:05:06+00:00", + "04:05:06+10", + "04:05:06+10:00", "04:05:06+10:30", + "04:05:06", + "10:23:54", + "00:00:00", + "10:23:54", + "10:23:54 BC", + "10:23:54", "10:23:54+1:2:3", "10:23:54+1:2", }, diff --git a/pkg/sql/pgwire/testdata/encodings.json b/pkg/sql/pgwire/testdata/encodings.json index 5f9cd3b9b7e7..e02e42f6947d 100644 --- a/pkg/sql/pgwire/testdata/encodings.json +++ b/pkg/sql/pgwire/testdata/encodings.json @@ -1665,6 +1665,118 @@ "TextAsBinary": [57, 48, 48, 52, 45, 49, 48, 45, 49, 57, 32, 49, 48, 58, 50, 51, 58, 53, 52], "Binary": [3, 17, 83, 233, 31, 54, 66, 128] }, + { + "SQL": "'1999-01-08 04:05:06+00'::timestamptz", + "Oid": 1184, + "Text": "1999-01-08 04:05:06+00", + "TextAsBinary": [49, 57, 57, 57, 45, 48, 49, 45, 48, 56, 32, 48, 52, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [255, 255, 227, 225, 177, 91, 128, 128] + }, + { + "SQL": "'1999-01-08 04:05:06+00:00'::timestamptz", + "Oid": 1184, + "Text": "1999-01-08 04:05:06+00", + "TextAsBinary": [49, 57, 57, 57, 45, 48, 49, 45, 48, 56, 32, 48, 52, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [255, 255, 227, 225, 177, 91, 128, 128] + }, + { + "SQL": "'1999-01-08 04:05:06+10'::timestamptz", + "Oid": 1184, + "Text": "1999-01-07 18:05:06+00", + "TextAsBinary": [49, 57, 57, 57, 45, 48, 49, 45, 48, 55, 32, 49, 56, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [255, 255, 227, 217, 79, 151, 24, 128] + }, + { + "SQL": "'1999-01-08 04:05:06+10:00'::timestamptz", + "Oid": 1184, + "Text": "1999-01-07 18:05:06+00", + "TextAsBinary": [49, 57, 57, 57, 45, 48, 49, 45, 48, 55, 32, 49, 56, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [255, 255, 227, 217, 79, 151, 24, 128] + }, + { + "SQL": "'1999-01-08 04:05:06+10:30'::timestamptz", + "Oid": 1184, + "Text": "1999-01-07 17:35:06+00", + "TextAsBinary": [49, 57, 57, 57, 45, 48, 49, 45, 48, 55, 32, 49, 55, 58, 51, 53, 58, 48, 54, 43, 48, 48], + "Binary": [255, 255, 227, 216, 228, 77, 70, 128] + }, + { + "SQL": "'1999-01-08 04:05:06'::timestamptz", + "Oid": 1184, + "Text": "1999-01-08 04:05:06+00", + "TextAsBinary": [49, 57, 57, 57, 45, 48, 49, 45, 48, 56, 32, 48, 52, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [255, 255, 227, 225, 177, 91, 128, 128] + }, + { + "SQL": "'2004-10-19 10:23:54'::timestamptz", + "Oid": 1184, + "Text": "2004-10-19 10:23:54+00", + "TextAsBinary": [50, 48, 48, 52, 45, 49, 48, 45, 49, 57, 32, 49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [0, 0, 137, 201, 15, 13, 226, 128] + }, + { + "SQL": "'0001-01-01 00:00:00'::timestamptz", + "Oid": 1184, + "Text": "0001-01-01 00:00:00+00", + "TextAsBinary": [48, 48, 48, 49, 45, 48, 49, 45, 48, 49, 32, 48, 48, 58, 48, 48, 58, 48, 48, 43, 48, 48], + "Binary": [255, 31, 226, 255, 197, 156, 96, 0] + }, + { + "SQL": "'0004-10-19 10:23:54'::timestamptz", + "Oid": 1184, + "Text": "0004-10-19 10:23:54+00", + "TextAsBinary": [48, 48, 48, 52, 45, 49, 48, 45, 49, 57, 32, 49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [255, 32, 80, 6, 42, 191, 2, 128] + }, + { + "SQL": "'0004-10-19 10:23:54 BC'::timestamptz", + "Oid": 1184, + "Text": "0004-10-19 10:23:54+00 BC", + "TextAsBinary": [48, 48, 48, 52, 45, 49, 48, 45, 49, 57, 32, 49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48, 32, 66, 67], + "Binary": [255, 31, 135, 24, 26, 133, 34, 128] + }, + { + "SQL": "'4004-10-19 10:23:54'::timestamptz", + "Oid": 1184, + "Text": "4004-10-19 10:23:54+00", + "TextAsBinary": [52, 48, 48, 52, 45, 49, 48, 45, 49, 57, 32, 49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [0, 224, 195, 139, 243, 92, 194, 128] + }, + { + "SQL": "'9004-10-19 10:23:54'::timestamptz", + "Oid": 1184, + "Text": "9004-10-19 10:23:54+00", + "TextAsBinary": [57, 48, 48, 52, 45, 49, 48, 45, 49, 57, 32, 49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [3, 17, 83, 233, 31, 54, 66, 128] + }, + { + "SQL": "'04:05:06+00'::timetz", + "Oid": 1266, + "Text": "04:05:06+00", + "TextAsBinary": [48, 52, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [0, 0, 0, 3, 108, 139, 192, 128, 0, 0, 0, 0] + }, + { + "SQL": "'04:05:06+00:00'::timetz", + "Oid": 1266, + "Text": "04:05:06+00", + "TextAsBinary": [48, 52, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [0, 0, 0, 3, 108, 139, 192, 128, 0, 0, 0, 0] + }, + { + "SQL": "'04:05:06+10'::timetz", + "Oid": 1266, + "Text": "04:05:06+10", + "TextAsBinary": [48, 52, 58, 48, 53, 58, 48, 54, 43, 49, 48], + "Binary": [0, 0, 0, 3, 108, 139, 192, 128, 255, 255, 115, 96] + }, + { + "SQL": "'04:05:06+10:00'::timetz", + "Oid": 1266, + "Text": "04:05:06+10", + "TextAsBinary": [48, 52, 58, 48, 53, 58, 48, 54, 43, 49, 48], + "Binary": [0, 0, 0, 3, 108, 139, 192, 128, 255, 255, 115, 96] + }, { "SQL": "'04:05:06+10:30'::timetz", "Oid": 1266, @@ -1672,6 +1784,48 @@ "TextAsBinary": [48, 52, 58, 48, 53, 58, 48, 54, 43, 49, 48, 58, 51, 48], "Binary": [0, 0, 0, 3, 108, 139, 192, 128, 255, 255, 108, 88] }, + { + "SQL": "'04:05:06'::timetz", + "Oid": 1266, + "Text": "04:05:06+00", + "TextAsBinary": [48, 52, 58, 48, 53, 58, 48, 54, 43, 48, 48], + "Binary": [0, 0, 0, 3, 108, 139, 192, 128, 0, 0, 0, 0] + }, + { + "SQL": "'10:23:54'::timetz", + "Oid": 1266, + "Text": "10:23:54+00", + "TextAsBinary": [49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [0, 0, 0, 8, 183, 61, 130, 128, 0, 0, 0, 0] + }, + { + "SQL": "'00:00:00'::timetz", + "Oid": 1266, + "Text": "00:00:00+00", + "TextAsBinary": [48, 48, 58, 48, 48, 58, 48, 48, 43, 48, 48], + "Binary": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + { + "SQL": "'10:23:54'::timetz", + "Oid": 1266, + "Text": "10:23:54+00", + "TextAsBinary": [49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [0, 0, 0, 8, 183, 61, 130, 128, 0, 0, 0, 0] + }, + { + "SQL": "'10:23:54 BC'::timetz", + "Oid": 1266, + "Text": "10:23:54+00", + "TextAsBinary": [49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [0, 0, 0, 8, 183, 61, 130, 128, 0, 0, 0, 0] + }, + { + "SQL": "'10:23:54'::timetz", + "Oid": 1266, + "Text": "10:23:54+00", + "TextAsBinary": [49, 48, 58, 50, 51, 58, 53, 52, 43, 48, 48], + "Binary": [0, 0, 0, 8, 183, 61, 130, 128, 0, 0, 0, 0] + }, { "SQL": "'10:23:54+1:2:3'::timetz", "Oid": 1266, diff --git a/pkg/sql/pgwire/testdata/pgtest/timezone b/pkg/sql/pgwire/testdata/pgtest/timezone index 312abe77f21e..ef98243f9b6b 100644 --- a/pkg/sql/pgwire/testdata/pgtest/timezone +++ b/pkg/sql/pgwire/testdata/pgtest/timezone @@ -33,16 +33,15 @@ ReadyForQuery {"Type":"CommandComplete","CommandTag":"SELECT 1"} {"Type":"ReadyForQuery","TxStatus":"I"} -# PostgreSQL does not display seconds offset here, but CockroachDB does. -send crdb_only +send Query {"String": "SELECT '1882-05-23T00:00:00'::\"timestamptz\""} ---- -until crdb_only ignore_data_type_sizes +until ignore_data_type_sizes ReadyForQuery ---- {"Type":"RowDescription","Fields":[{"Name":"timestamptz","TableOID":0,"TableAttributeNumber":0,"DataTypeOID":1184,"DataTypeSize":0,"TypeModifier":-1,"Format":0}]} -{"Type":"DataRow","Values":[{"text":"1882-05-23 00:00:00+00:00"}]} +{"Type":"DataRow","Values":[{"text":"1882-05-23 00:00:00+00"}]} {"Type":"CommandComplete","CommandTag":"SELECT 1"} {"Type":"ReadyForQuery","TxStatus":"I"} diff --git a/pkg/sql/pgwire/types.go b/pkg/sql/pgwire/types.go index a0f52d27f659..b7ce09d80531 100644 --- a/pkg/sql/pgwire/types.go +++ b/pkg/sql/pgwire/types.go @@ -527,10 +527,10 @@ func (b *writeBuffer) writeBinaryDatum( const ( pgTimeFormat = "15:04:05.999999" - pgTimeTZFormat = pgTimeFormat + "-07:00" + pgTimeTZFormat = pgTimeFormat + "-07" pgDateFormat = "2006-01-02" pgTimeStampFormatNoOffset = pgDateFormat + " " + pgTimeFormat - pgTimeStampFormat = pgTimeStampFormatNoOffset + "-07:00" + pgTimeStampFormat = pgTimeStampFormatNoOffset + "-07" pgTime2400Format = "24:00:00" ) @@ -548,11 +548,11 @@ func formatTime(t timeofday.TimeOfDay, tmp []byte) []byte { // formatTimeTZ formats t into a format lib/pq understands, appending to the // provided tmp buffer and reallocating if needed. The function will then return // the resulting buffer. -// Note it does not understand the "second" component of the offset as lib/pq -// cannot parse it. func formatTimeTZ(t timetz.TimeTZ, tmp []byte) []byte { format := pgTimeTZFormat if t.OffsetSecs%60 != 0 { + format += ":00:00" + } else if t.OffsetSecs%3600 != 0 { format += ":00" } ret := t.ToTime().AppendFormat(tmp, format) @@ -571,7 +571,9 @@ func formatTs(t time.Time, offset *time.Location, tmp []byte) (b []byte) { var format string if offset != nil { format = pgTimeStampFormat - if _, offset := t.In(offset).Zone(); offset%60 != 0 { + if _, offsetSeconds := t.In(offset).Zone(); offsetSeconds%60 != 0 { + format += ":00:00" + } else if offsetSeconds%3600 != 0 { format += ":00" } } else { From f8cc40977557d18487332c824d26ad65e3423071 Mon Sep 17 00:00:00 2001 From: Rafi Shamim Date: Wed, 12 May 2021 20:09:33 -0400 Subject: [PATCH 2/3] cli: format time zones and floating points to match SQL Release note (cli change): The SQL shell now formats times with time zones so that the minutes and seconds offsets are only shown if they are non-zero. Also, infinite floating point values are formatted as `Infinity` rather than `Inf` now. --- pkg/cli/cli_test.go | 17 +++++++++++------ pkg/cli/sql_util.go | 15 ++++++++++++++- pkg/util/timeutil/timeutil.go | 14 ++++++++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index 86051477cf35..e5ea810320fe 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -620,8 +620,9 @@ func Example_sql_format() { c.RunWithArgs([]string{"sql", "-e", "create database t; create table t.times (bare timestamp, withtz timestamptz)"}) c.RunWithArgs([]string{"sql", "-e", "insert into t.times values ('2016-01-25 10:10:10', '2016-01-25 10:10:10-05:00')"}) c.RunWithArgs([]string{"sql", "-e", "select bare from t.times; select withtz from t.times"}) - c.RunWithArgs([]string{"sql", "-e", "select '2021-03-20'::date; select '01:01'::time; select '01:01'::timetz"}) - c.RunWithArgs([]string{"sql", "-e", "select (1/3.0)::real; select (1/3.0)::double precision"}) + 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"}) // Output: // sql -e create database t; create table t.times (bare timestamp, withtz timestamptz) @@ -632,19 +633,23 @@ func Example_sql_format() { // bare // 2016-01-25 10:10:10 // withtz - // 2016-01-25 15:10:10+00:00:00 - // sql -e select '2021-03-20'::date; select '01:01'::time; select '01:01'::timetz + // 2016-01-25 15:10:10+00 + // sql -e select '2021-03-20'::date; select '01:01'::time; select '01:01'::timetz; select '01:01+02:02'::timetz // date // 2021-03-20 // time // 01:01:00 // timetz - // 01:01:00+00:00:00 - // sql -e select (1/3.0)::real; select (1/3.0)::double precision + // 01:01:00+00 + // timetz + // 01:01:00+02:02 + // sql -e select (1/3.0)::real; select (1/3.0)::double precision; select '-inf'::float8 // float4 // 0.33333334 // float8 // 0.3333333333333333 + // float8 + // -Infinity } func Example_sql_column_labels() { diff --git a/pkg/cli/sql_util.go b/pkg/cli/sql_util.go index 5dbf165bdad2..4e5522e67d0d 100644 --- a/pkg/cli/sql_util.go +++ b/pkg/cli/sql_util.go @@ -15,6 +15,7 @@ import ( "database/sql/driver" "fmt" "io" + "math" "net/url" "reflect" "strconv" @@ -1134,6 +1135,11 @@ func formatVal( if colType == "FLOAT4" { width = 32 } + if math.IsInf(t, 1) { + return "Infinity" + } else if math.IsInf(t, -1) { + return "-Infinity" + } return strconv.FormatFloat(t, 'g', -1, width) case string: @@ -1178,6 +1184,13 @@ func formatVal( // Some unknown/new time-like format. tfmt = timeutil.FullTimeFormat } + if tfmt == timeutil.TimestampWithTZFormat || tfmt == timeutil.TimeWithTZFormat { + if _, offsetSeconds := t.Zone(); offsetSeconds%60 != 0 { + tfmt += ":00:00" + } else if offsetSeconds%3600 != 0 { + tfmt += ":00" + } + } return t.Format(tfmt) } @@ -1186,7 +1199,7 @@ func formatVal( var timeOutputFormats = map[string]string{ "TIMESTAMP": timeutil.TimestampWithoutTZFormat, - "TIMESTAMPTZ": timeutil.FullTimeFormat, + "TIMESTAMPTZ": timeutil.TimestampWithTZFormat, "TIME": timeutil.TimeWithoutTZFormat, "TIMETZ": timeutil.TimeWithTZFormat, "DATE": timeutil.DateFormat, diff --git a/pkg/util/timeutil/timeutil.go b/pkg/util/timeutil/timeutil.go index bb6c7b51f597..07d846a598a0 100644 --- a/pkg/util/timeutil/timeutil.go +++ b/pkg/util/timeutil/timeutil.go @@ -10,17 +10,23 @@ package timeutil -// FullTimeFormat is the time format used to display any timestamp -// with date, time and time zone data. +// FullTimeFormat is the time format used to display any unknown timestamp +// type, and always shows the full time zone offset. const FullTimeFormat = "2006-01-02 15:04:05.999999-07:00:00" +// TimestampWithTZFormat is the time format used to display +// timestamps with a time zone offset. The minutes and seconds +// offsets are only added if they are non-zero. +const TimestampWithTZFormat = "2006-01-02 15:04:05.999999-07" + // TimestampWithoutTZFormat is the time format used to display -// timestamps without a time zone offset. +// timestamps without a time zone offset. The minutes and seconds +// offsets are only added if they are non-zero. const TimestampWithoutTZFormat = "2006-01-02 15:04:05.999999" // TimeWithTZFormat is the time format used to display a time // with a time zone offset. -const TimeWithTZFormat = "15:04:05.999999-07:00:00" +const TimeWithTZFormat = "15:04:05.999999-07" // TimeWithoutTZFormat is the time format used to display a time // without a time zone offset. From aba5a3d03940881db8f5616ead426d67ee51e0d1 Mon Sep 17 00:00:00 2001 From: Raphael 'kena' Poss Date: Fri, 4 Jun 2021 13:19:48 +0200 Subject: [PATCH 3/3] cli/sql: properly format common array types Release note (bug fix): CockroachDB's SQL shell now properly displays results of common array types, for example arrays of floats or arrays of strings. --- pkg/cli/cli_test.go | 30 +++++++++++++++ pkg/cli/sql_util.go | 92 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index e5ea810320fe..b186bddf72b8 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -623,6 +623,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) @@ -650,6 +660,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() { diff --git a/pkg/cli/sql_util.go b/pkg/cli/sql_util.go index 4e5522e67d0d..96cf95bdbd75 100644 --- a/pkg/cli/sql_util.go +++ b/pkg/cli/sql_util.go @@ -12,12 +12,14 @@ package cli import ( "context" + gosql "database/sql" "database/sql/driver" "fmt" "io" "math" "net/url" "reflect" + "regexp" "strconv" "strings" "time" @@ -1121,9 +1123,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) { @@ -1197,6 +1207,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 = "," + } + buf.WriteByte('}') + return buf.String() +} + +var reArrayStringEscape = regexp.MustCompile(`(["\\])`) + var timeOutputFormats = map[string]string{ "TIMESTAMP": timeutil.TimestampWithoutTZFormat, "TIMESTAMPTZ": timeutil.TimestampWithTZFormat,