Skip to content
Open
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
12 changes: 12 additions & 0 deletions pgtype/range_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pgtype
import (
"database/sql/driver"
"fmt"
"strings"

"github.com/jackc/pgx/v5/internal/pgio"
)
Expand Down Expand Up @@ -192,13 +193,15 @@ 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)
}
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, ',')
Expand All @@ -213,13 +216,15 @@ 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)
}
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 {
Expand All @@ -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:
Expand Down
67 changes: 67 additions & 0 deletions pgtype/range_codec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}