From 8a4282465553e12ea70e0f4a48264f7c4e537cb2 Mon Sep 17 00:00:00 2001 From: niger Date: Wed, 18 Mar 2026 16:50:46 -0400 Subject: [PATCH 1/6] add option to enable Redshift OIDs --- connector.go | 4 ++ connector_test.go | 1 + rows.go | 111 +++++++++++++++++++++++++++++++++++++++++++++- rows_test.go | 35 +++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/connector.go b/connector.go index 9f4f2ca36..3839b2338 100644 --- a/connector.go +++ b/connector.go @@ -372,6 +372,10 @@ type Config struct { // supported in libpq. BinaryParameters bool `postgres:"binary_parameters" env:"-"` + // When true, Redshift type mapping is enabled for ColumnTypeDatabaseTypeName. + // This includes OIDs mapped to _spectrum_array, unknown types, etc. + RedshiftOIDs bool `postgres:"redshift_oids" env:"PGREDSHIFTOIDS"` + // This connection should never use the binary format when receiving query // results from prepared statements. Only provided for debugging. This is a // pq extension, not supported in libpq. diff --git a/connector_test.go b/connector_test.go index 87a4e9ce6..1a8e4b6a4 100644 --- a/connector_test.go +++ b/connector_test.go @@ -374,6 +374,7 @@ func TestNewConfig(t *testing.T) { {"sslinline=yes", nil, "sslinline=yes", ""}, {"sslinline=no", nil, "sslinline=no", ""}, {"sslinline=lol", nil, "", `pq: wrong value for "sslinline": strconv.ParseBool: parsing "lol": invalid syntax`}, + {"redshift_oids=1", nil, "redshift_oids=yes", ""}, // application_name and fallback_application_name {"application_name=acme", nil, "application_name=acme", ""}, diff --git a/rows.go b/rows.go index 2029bfed2..422db245b 100644 --- a/rows.go +++ b/rows.go @@ -161,7 +161,13 @@ func (rs *rows) ColumnTypeScanType(index int) reflect.Type { // ColumnTypeDatabaseTypeName return the database system type name. func (rs *rows) ColumnTypeDatabaseTypeName(index int) string { - return rs.colTyps[index].Name() + name := rs.colTyps[index].Name() + if name == "" && rs.cn.cfg.RedshiftOIDs { + if mapped, ok := redshiftTypeName[rs.colTyps[index].OID]; ok { + return mapped + } + } + return name } // ColumnTypeLength returns the length of the column type if the column is a @@ -243,3 +249,106 @@ func (fd fieldDesc) PrecisionScale() (precision, scale int64, ok bool) { return 0, 0, false } } + +var redshiftTypeName = map[oid.Oid]string{ + 16: "BOOL", + 17: "BYTEA", + 18: "CHAR", + 19: "NAME", + 21: "INT2", + 22: "INT2VECTOR", + 23: "INT4", + 24: "REGPROC", + 25: "TEXT", + 26: "OID", + 28: "XID", + 29: "CID", + 30: "OIDVECTOR", + 71: "PG_TYPE", + 75: "PG_ATTRIBUTE", + 81: "PG_PROC", + 83: "PG_CLASS", + 86: "PG_SHADOW", + 87: "PG_GROUP", + 88: "PG_DATABASE", + 90: "PG_TABLESPACE", + 210: "SMGR", + 600: "POINT", + 601: "LSEG", + 602: "PATH", + 603: "BOX", + 604: "POLYGON", + 628: "LINE", + 629: "_LINE", + 635: "_SPECTRUM_ARRAY", + 636: "_SPECTRUM_MAP", + 637: "_SPECTRUM_STRUCT", + 702: "ABSTIME", + 703: "RELTIME", + 704: "TINTERVAL", + 705: "UNKNOWN", + 718: "CIRCLE", + 719: "_CIRCLE", + 790: "MONEY", + 791: "_MONEY", + 829: "MACADDR", + 869: "INET", + 650: "CIDR", + 1000: "_BOOL", + 1001: "_BYTEA", + 1002: "_CHAR", + 1003: "_NAME", + 1005: "_INT2", + 1006: "_INT2VECTOR", + 1007: "_INT4", + 1008: "_REGPROC", + 1009: "_TEXT", + 1028: "_OID", + 1010: "_TID", + 1011: "_XID", + 1012: "_CID", + 1013: "_OIDVECTOR", + 1014: "_BPCHAR", + 1015: "_VARCHAR", + 1016: "_INT8", + 1017: "_POINT", + 1018: "_LSEG", + 1019: "_PATH", + 1020: "_BOX", + 1021: "_FLOAT4", + 1022: "_FLOAT8", + 1023: "_ABSTIME", + 1024: "_RELTIME", + 1025: "_TINTERVAL", + 1027: "_POLYGON", + 1034: "_ACLITEM", + 1040: "_MACADDR", + 1041: "_INET", + 651: "_CIDR", + 1042: "BPCHAR", + 1043: "VARCHAR", + 1082: "DATE", + 1115: "_TIMESTAMP", + 1182: "_DATE", + 1183: "_TIME", + 1185: "_TIMESTAMPTZ", + 1186: "INTERVAL", + 1187: "_INTERVAL", + 1231: "_NUMERIC", + 1266: "TIMETZ", + 1270: "_TIMETZ", + 1560: "BIT", + 1561: "_BIT", + 1562: "VARBIT", + 1563: "_VARBIT", + 1700: "NUMERIC", + 1790: "REFCURSOR", + 2201: "_REFCURSOR", + 2202: "REGPROCEDURE", + 2203: "REGOPER", + 2204: "REGOPERATOR", + 2205: "REGCLASS", + 2206: "REGTYPE", + 2207: "_REGPROCEDURE", + 2208: "_REGOPER", +} diff --git a/rows_test.go b/rows_test.go index 56d8a6f9a..f94495c0d 100644 --- a/rows_test.go +++ b/rows_test.go @@ -41,6 +41,41 @@ func TestDataTypeName(t *testing.T) { } } +func TestDataTypeNameRedshift(t *testing.T) { + tts := []struct { + typ oid.Oid + name string + }{ + {635, "_SPECTRUM_ARRAY"}, + {636, "_SPECTRUM_MAP"}, + {637, "_SPECTRUM_STRUCT"}, + {16, "BOOL"}, + } + + rs := &rows{ + cn: &conn{ + cfg: Config{RedshiftOIDs: true}, + }, + rowsHeader: rowsHeader{ + colTyps: []fieldDesc{}, + }, + } + + for i, tt := range tts { + rs.colTyps = []fieldDesc{{OID: tt.typ}} + if name := rs.ColumnTypeDatabaseTypeName(0); name != tt.name { + t.Errorf("(%d) got: %s want: %s", i, name, tt.name) + } + } + + // Without Redshift flag + rs.cn.cfg.RedshiftOIDs = false + rs.colTyps = []fieldDesc{{OID: 635}} + if name := rs.ColumnTypeDatabaseTypeName(0); name != "" { + t.Errorf("got: %q want: %q", name, "") // 635 is not in standard types map, so it returns empty string + } +} + func TestDataType(t *testing.T) { tts := []struct { typ oid.Oid From d8d86bf85e2b600b8cf38a9ba531b48556219df4 Mon Sep 17 00:00:00 2001 From: niger Date: Wed, 18 Mar 2026 18:00:33 -0400 Subject: [PATCH 2/6] fix --- rows.go | 108 ++++++++------------------------------------------------ 1 file changed, 14 insertions(+), 94 deletions(-) diff --git a/rows.go b/rows.go index 422db245b..33bd120d5 100644 --- a/rows.go +++ b/rows.go @@ -162,7 +162,7 @@ func (rs *rows) ColumnTypeScanType(index int) reflect.Type { // ColumnTypeDatabaseTypeName return the database system type name. func (rs *rows) ColumnTypeDatabaseTypeName(index int) string { name := rs.colTyps[index].Name() - if name == "" && rs.cn.cfg.RedshiftOIDs { + if name == "" && rs.cn != nil && rs.cn.cfg.RedshiftOIDs { if mapped, ok := redshiftTypeName[rs.colTyps[index].OID]; ok { return mapped } @@ -251,104 +251,24 @@ func (fd fieldDesc) PrecisionScale() (precision, scale int64, ok bool) { } var redshiftTypeName = map[oid.Oid]string{ - 16: "BOOL", - 17: "BYTEA", - 18: "CHAR", - 19: "NAME", - 21: "INT2", - 22: "INT2VECTOR", - 23: "INT4", - 24: "REGPROC", - 25: "TEXT", - 26: "OID", - 28: "XID", - 29: "CID", - 30: "OIDVECTOR", - 71: "PG_TYPE", - 75: "PG_ATTRIBUTE", - 81: "PG_PROC", - 83: "PG_CLASS", 86: "PG_SHADOW", 87: "PG_GROUP", 88: "PG_DATABASE", 90: "PG_TABLESPACE", - 210: "SMGR", - 600: "POINT", - 601: "LSEG", - 602: "PATH", - 603: "BOX", - 604: "POLYGON", - 628: "LINE", - 629: "_LINE", 635: "_SPECTRUM_ARRAY", 636: "_SPECTRUM_MAP", 637: "_SPECTRUM_STRUCT", - 702: "ABSTIME", - 703: "RELTIME", - 704: "TINTERVAL", - 705: "UNKNOWN", - 718: "CIRCLE", - 719: "_CIRCLE", - 790: "MONEY", - 791: "_MONEY", - 829: "MACADDR", - 869: "INET", - 650: "CIDR", - 1000: "_BOOL", - 1001: "_BYTEA", - 1002: "_CHAR", - 1003: "_NAME", - 1005: "_INT2", - 1006: "_INT2VECTOR", - 1007: "_INT4", - 1008: "_REGPROC", - 1009: "_TEXT", - 1028: "_OID", - 1010: "_TID", - 1011: "_XID", - 1012: "_CID", - 1013: "_OIDVECTOR", - 1014: "_BPCHAR", - 1015: "_VARCHAR", - 1016: "_INT8", - 1017: "_POINT", - 1018: "_LSEG", - 1019: "_PATH", - 1020: "_BOX", - 1021: "_FLOAT4", - 1022: "_FLOAT8", - 1023: "_ABSTIME", - 1024: "_RELTIME", - 1025: "_TINTERVAL", - 1027: "_POLYGON", - 1034: "_ACLITEM", - 1040: "_MACADDR", - 1041: "_INET", - 651: "_CIDR", - 1042: "BPCHAR", - 1043: "VARCHAR", - 1082: "DATE", - 1115: "_TIMESTAMP", - 1182: "_DATE", - 1183: "_TIME", - 1185: "_TIMESTAMPTZ", - 1186: "INTERVAL", - 1187: "_INTERVAL", - 1231: "_NUMERIC", - 1266: "TIMETZ", - 1270: "_TIMETZ", - 1560: "BIT", - 1561: "_BIT", - 1562: "VARBIT", - 1563: "_VARBIT", - 1700: "NUMERIC", - 1790: "REFCURSOR", - 2201: "_REFCURSOR", - 2202: "REGPROCEDURE", - 2203: "REGOPER", - 2204: "REGOPERATOR", - 2205: "REGCLASS", - 2206: "REGTYPE", - 2207: "_REGPROCEDURE", - 2208: "_REGOPER", + 1188: "INTERVALY2M", + 1189: "_INTERVALY2M", + 1190: "INTERVALD2S", + 1191: "_INTERVALD2S", + 2935: "HLLSKETCH", + 3000: "GEOMETRY", + 3001: "GEOGRAPHY", + 4000: "SUPER", + 4600: "USERITEM", + 4601: "_USERITEM", + 4602: "ROLEITEM", + 4603: "_ROLEITEM", + 6551: "VARBYTE", } From 37dc27560241ec20f15ead5538141554d360d645 Mon Sep 17 00:00:00 2001 From: niger Date: Wed, 18 Mar 2026 18:03:33 -0400 Subject: [PATCH 3/6] replace with better case for test --- rows_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rows_test.go b/rows_test.go index f94495c0d..36c43e2b4 100644 --- a/rows_test.go +++ b/rows_test.go @@ -49,7 +49,7 @@ func TestDataTypeNameRedshift(t *testing.T) { {635, "_SPECTRUM_ARRAY"}, {636, "_SPECTRUM_MAP"}, {637, "_SPECTRUM_STRUCT"}, - {16, "BOOL"}, + {4000, "SUPER"}, } rs := &rows{ From 1b2b810499bd5222e049826cfb735f1202763ffb Mon Sep 17 00:00:00 2001 From: niger Date: Wed, 18 Mar 2026 19:33:37 -0400 Subject: [PATCH 4/6] switch to dynamic strategy --- conn.go | 3 +++ rows.go | 2 +- rows_test.go | 4 ++-- test_rs_test.go | 23 +++++++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 test_rs_test.go diff --git a/conn.go b/conn.go index cab8f3753..75a3842ad 100644 --- a/conn.go +++ b/conn.go @@ -87,6 +87,7 @@ type parameterStatus struct { serverVersion int currentLocation *time.Location inHotStandby, defaultTransactionReadOnly sql.NullBool + isRedshift bool } type format int @@ -1554,6 +1555,8 @@ func (cn *conn) processParameterStatus(r *readBuf) { switch r.string() { default: // ignore + case "padb_version": + cn.parameterStatus.isRedshift = true case "server_version": var major1, major2 int _, err := fmt.Sscanf(r.string(), "%d.%d", &major1, &major2) diff --git a/rows.go b/rows.go index 33bd120d5..af00f4b23 100644 --- a/rows.go +++ b/rows.go @@ -162,7 +162,7 @@ func (rs *rows) ColumnTypeScanType(index int) reflect.Type { // ColumnTypeDatabaseTypeName return the database system type name. func (rs *rows) ColumnTypeDatabaseTypeName(index int) string { name := rs.colTyps[index].Name() - if name == "" && rs.cn != nil && rs.cn.cfg.RedshiftOIDs { + if name == "" && rs.cn != nil && rs.cn.parameterStatus.isRedshift { if mapped, ok := redshiftTypeName[rs.colTyps[index].OID]; ok { return mapped } diff --git a/rows_test.go b/rows_test.go index 36c43e2b4..71ae34823 100644 --- a/rows_test.go +++ b/rows_test.go @@ -54,7 +54,7 @@ func TestDataTypeNameRedshift(t *testing.T) { rs := &rows{ cn: &conn{ - cfg: Config{RedshiftOIDs: true}, + parameterStatus: parameterStatus{isRedshift: true}, }, rowsHeader: rowsHeader{ colTyps: []fieldDesc{}, @@ -69,7 +69,7 @@ func TestDataTypeNameRedshift(t *testing.T) { } // Without Redshift flag - rs.cn.cfg.RedshiftOIDs = false + rs.cn.parameterStatus.isRedshift = false rs.colTyps = []fieldDesc{{OID: 635}} if name := rs.ColumnTypeDatabaseTypeName(0); name != "" { t.Errorf("got: %q want: %q", name, "") // 635 is not in standard types map, so it returns empty string diff --git a/test_rs_test.go b/test_rs_test.go new file mode 100644 index 000000000..0d050a3b7 --- /dev/null +++ b/test_rs_test.go @@ -0,0 +1,23 @@ +package pq + +import ( + "database/sql" + "testing" +) + +func TestRedshiftParams(t *testing.T) { + dsn := "postgres://scantron:520OkwZT6z5G@scantron.239674069128.us-east-1.redshift-serverless.amazonaws.com:5439/dev?sslmode=require" + db, err := sql.Open("postgres", dsn) + if err != nil { + t.Fatalf("Failed to open DB: %v", err) + } + defer db.Close() + + rows, err := db.Query("SELECT 1") + if err != nil { + t.Fatalf("Query failed: %v", err) + } + defer rows.Close() + + t.Log("Done") +} From 2e3a7cba9551b7fd585707410c6588a5795765e9 Mon Sep 17 00:00:00 2001 From: niger Date: Wed, 18 Mar 2026 19:36:40 -0400 Subject: [PATCH 5/6] remove dead code --- connector.go | 4 ---- connector_test.go | 1 - test_rs_test.go | 23 ----------------------- 3 files changed, 28 deletions(-) delete mode 100644 test_rs_test.go diff --git a/connector.go b/connector.go index 3839b2338..9f4f2ca36 100644 --- a/connector.go +++ b/connector.go @@ -372,10 +372,6 @@ type Config struct { // supported in libpq. BinaryParameters bool `postgres:"binary_parameters" env:"-"` - // When true, Redshift type mapping is enabled for ColumnTypeDatabaseTypeName. - // This includes OIDs mapped to _spectrum_array, unknown types, etc. - RedshiftOIDs bool `postgres:"redshift_oids" env:"PGREDSHIFTOIDS"` - // This connection should never use the binary format when receiving query // results from prepared statements. Only provided for debugging. This is a // pq extension, not supported in libpq. diff --git a/connector_test.go b/connector_test.go index 1a8e4b6a4..87a4e9ce6 100644 --- a/connector_test.go +++ b/connector_test.go @@ -374,7 +374,6 @@ func TestNewConfig(t *testing.T) { {"sslinline=yes", nil, "sslinline=yes", ""}, {"sslinline=no", nil, "sslinline=no", ""}, {"sslinline=lol", nil, "", `pq: wrong value for "sslinline": strconv.ParseBool: parsing "lol": invalid syntax`}, - {"redshift_oids=1", nil, "redshift_oids=yes", ""}, // application_name and fallback_application_name {"application_name=acme", nil, "application_name=acme", ""}, diff --git a/test_rs_test.go b/test_rs_test.go deleted file mode 100644 index 0d050a3b7..000000000 --- a/test_rs_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package pq - -import ( - "database/sql" - "testing" -) - -func TestRedshiftParams(t *testing.T) { - dsn := "postgres://scantron:520OkwZT6z5G@scantron.239674069128.us-east-1.redshift-serverless.amazonaws.com:5439/dev?sslmode=require" - db, err := sql.Open("postgres", dsn) - if err != nil { - t.Fatalf("Failed to open DB: %v", err) - } - defer db.Close() - - rows, err := db.Query("SELECT 1") - if err != nil { - t.Fatalf("Query failed: %v", err) - } - defer rows.Close() - - t.Log("Done") -} From 3aaec9c373fc4b0f31870628adfc9b7630563fa5 Mon Sep 17 00:00:00 2001 From: niger Date: Thu, 19 Mar 2026 10:57:13 -0400 Subject: [PATCH 6/6] address nit --- rows.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rows.go b/rows.go index af00f4b23..60a613471 100644 --- a/rows.go +++ b/rows.go @@ -161,13 +161,12 @@ func (rs *rows) ColumnTypeScanType(index int) reflect.Type { // ColumnTypeDatabaseTypeName return the database system type name. func (rs *rows) ColumnTypeDatabaseTypeName(index int) string { - name := rs.colTyps[index].Name() - if name == "" && rs.cn != nil && rs.cn.parameterStatus.isRedshift { - if mapped, ok := redshiftTypeName[rs.colTyps[index].OID]; ok { - return mapped + if rs.cn.parameterStatus.isRedshift { + if n, ok := redshiftTypeName[rs.colTyps[index].OID]; ok { + return n } } - return name + return rs.colTyps[index].Name() } // ColumnTypeLength returns the length of the column type if the column is a