diff --git a/pgtype/range_codec.go b/pgtype/range_codec.go index dc1ac8b06..619b575c5 100644 --- a/pgtype/range_codec.go +++ b/pgtype/range_codec.go @@ -3,6 +3,7 @@ package pgtype import ( "database/sql/driver" "fmt" + "strings" "github.com/jackc/pgx/v5/internal/pgio" ) @@ -192,6 +193,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) return nil, fmt.Errorf("cannot encode %v as element of range", lower) } + boundStart := len(buf) buf, err = lowerPlan.Encode(lower, buf) if err != nil { return nil, fmt.Errorf("failed to encode %v as element of range: %w", lower, err) @@ -199,6 +201,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) if buf == nil { return nil, fmt.Errorf("Lower cannot be NULL unless LowerType is Unbounded") } + buf = append(buf[:boundStart], quoteRangeBoundIfNeeded(string(buf[boundStart:]))...) } buf = append(buf, ',') @@ -213,6 +216,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) return nil, fmt.Errorf("cannot encode %v as element of range", upper) } + boundStart := len(buf) buf, err = upperPlan.Encode(upper, buf) if err != nil { return nil, fmt.Errorf("failed to encode %v as element of range: %w", upper, err) @@ -220,6 +224,7 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) if buf == nil { return nil, fmt.Errorf("Upper cannot be NULL unless UpperType is Unbounded") } + buf = append(buf[:boundStart], quoteRangeBoundIfNeeded(string(buf[boundStart:]))...) } switch upperType { @@ -234,6 +239,13 @@ func (plan *encodePlanRangeCodecRangeValuerToText) Encode(value any, buf []byte) return buf, nil } +func quoteRangeBoundIfNeeded(src string) string { + if src == "" || strings.ContainsAny(src, "\"\\,()[]") { + return quoteArrayElement(src) + } + return src +} + func (c *RangeCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan { switch format { case BinaryFormatCode: diff --git a/pgtype/range_codec_test.go b/pgtype/range_codec_test.go index 6769a74b7..477261461 100644 --- a/pgtype/range_codec_test.go +++ b/pgtype/range_codec_test.go @@ -159,3 +159,70 @@ func TestRangeCodecDecodeValue(t *testing.T) { } }) } + +func TestRangeCodecTextBounds(t *testing.T) { + skipCockroachDB(t, "Server does not support range types") + defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { + _, err := conn.Exec(ctx, `create type pg_temp.text_bounds_range as range (subtype = text, collation = "C")`) + require.NoError(t, err) + dt, err := conn.LoadType(ctx, "pg_temp.text_bounds_range") + require.NoError(t, err) + conn.TypeMap().RegisterType(dt) + + for _, bound := range []string{"", "a", "a,b", `a"b`, `a\b`, "a(b", "a)b", "a[b", "a]b", "a b", "a\tb", "a\nb", "a\rb", "a\vb", "a\fb", "a{b", "a}b"} { + for _, lower := range []bool{true, false} { + input := pgtype.Range[string]{Lower: bound, Upper: bound, LowerType: pgtype.Inclusive, UpperType: pgtype.Inclusive, Valid: true} + if lower { + input.UpperType = pgtype.Unbounded + } else { + input.LowerType = pgtype.Unbounded + } + encoded, err := conn.TypeMap().Encode(dt.OID, pgtype.TextFormatCode, input, []byte("prefix:")) + require.NoError(t, err) + require.Equal(t, "prefix:", string(encoded[:7])) + + var actual string + var infinite bool + query := `select lower(r), lower_inf(r) from (select $1::text::pg_temp.text_bounds_range r) s` + if !lower { + query = `select upper(r), upper_inf(r) from (select $1::text::pg_temp.text_bounds_range r) s` + } + err = conn.QueryRow(ctx, query, string(encoded[7:])).Scan(&actual, &infinite) + if err != nil { + t.Errorf("bound %q lower=%v encoded=%q: %v", bound, lower, encoded[7:], err) + continue + } + require.False(t, infinite, "bound %q lower=%v", bound, lower) + require.Equal(t, bound, actual, "lower=%v", lower) + } + } + }) +} + +func TestRangeCodecTextFiniteBounds(t *testing.T) { + skipCockroachDB(t, "Server does not support range types") + defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) { + _, err := conn.Exec(ctx, `create type pg_temp.text_finite_bounds_range as range (subtype = text, collation = "C")`) + require.NoError(t, err) + dt, err := conn.LoadType(ctx, "pg_temp.text_finite_bounds_range") + require.NoError(t, err) + conn.TypeMap().RegisterType(dt) + + input := pgtype.Range[string]{Lower: `a"\,([`, Upper: `z"\,)]`, LowerType: pgtype.Inclusive, UpperType: pgtype.Exclusive, Valid: true} + for _, capacity := range []int{7, 256} { + buf := make([]byte, 7, capacity) + copy(buf, "prefix:") + encoded, err := conn.TypeMap().Encode(dt.OID, pgtype.TextFormatCode, input, buf) + require.NoError(t, err) + require.Equal(t, "prefix:", string(encoded[:7])) + var lower, upper string + var lowerInclusive, upperInclusive bool + err = conn.QueryRow(ctx, `select lower(r), upper(r), lower_inc(r), upper_inc(r) from (select $1::text::pg_temp.text_finite_bounds_range r) s`, string(encoded[7:])).Scan(&lower, &upper, &lowerInclusive, &upperInclusive) + require.NoError(t, err, "capacity=%d encoded=%q", capacity, encoded[7:]) + require.Equal(t, input.Lower, lower) + require.Equal(t, input.Upper, upper) + require.True(t, lowerInclusive) + require.False(t, upperInclusive) + } + }) +}