Skip to content
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
101 changes: 101 additions & 0 deletions copy_from_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxtest"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -452,6 +453,106 @@ func TestConnCopyFromJSON(t *testing.T) {
ensureConnValid(t, conn)
}

func TestConnCopyFromTSVector(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()

conn := mustConnectString(t, os.Getenv("PGX_TEST_DATABASE"))
defer closeConn(t, conn)

pgxtest.SkipCockroachDB(t, conn, "CockroachDB handles tsvector escaping differently")

tx, err := conn.Begin(ctx)
require.NoError(t, err)
defer tx.Rollback(ctx)

_, err = tx.Exec(ctx, `create temporary table tmp_tsv (id int, t tsvector)`)
require.NoError(t, err)

inputRows := [][]any{
// Text format: core functionality.
{1, `'a':1A 'cat':5 'fat':2B,4C`}, // Multiple lexemes with positions and weights.
{2, `'bare'`}, // Single lexeme with no positions.
{3, `'multi':1,2,3,4,5`}, // Multiple positions (default weight D).
{4, `'test':1A,2B,3C,4D`}, // All four weights on one lexeme.
{5, `'word':1D`}, // Explicit weight D (normalizes to no suffix).
{6, `'high':16383A`}, // High position number (near 14-bit max).

// Text format: escaping.
{7, `'don''t'`}, // Quote escaping (doubled single quote).
{8, `'don\'t'`}, // Quote escaping (backslash).
{9, `'ab\\c'`}, // Backslash in lexeme.
{10, `'\ foo'`}, // Escaped space.

// Text format: special characters.
{11, `'café' 'naïve'`}, // Unicode lexemes.
{12, `'a:b' 'c,d'`}, // Delimiter-like characters (colon, comma).

// Struct format: tests binary encoding path.
{13, pgtype.TSVector{
Lexemes: []pgtype.TSVectorLexeme{
{Word: "alpha", Positions: []pgtype.TSVectorPosition{{Position: 1, Weight: pgtype.TSVectorWeightA}}},
{Word: "beta", Positions: []pgtype.TSVectorPosition{{Position: 2, Weight: pgtype.TSVectorWeightB}}},
{Word: "gamma", Positions: nil},
},
Valid: true,
}},
{14, pgtype.TSVector{Valid: true}}, // Empty valid tsvector (no lexemes).

// NULL handling.
{15, pgtype.TSVector{Valid: false}}, // Invalid (NULL) TSVector struct.
{16, nil}, // Nil value.
}

copyCount, err := conn.CopyFrom(ctx, pgx.Identifier{"tmp_tsv"}, []string{"id", "t"}, pgx.CopyFromRows(inputRows))
require.NoError(t, err)
require.EqualValues(t, len(inputRows), copyCount)

rows, err := conn.Query(ctx, "select id, t::text from tmp_tsv order by id nulls last")
require.NoError(t, err)

var outputRows [][]any
for rows.Next() {
row, err := rows.Values()
require.NoError(t, err)
outputRows = append(outputRows, row)
}
require.NoError(t, rows.Err())

expectedOutputRows := [][]any{
// Text format: core functionality.
{int32(1), `'a':1A 'cat':5 'fat':2B,4C`},
{int32(2), `'bare'`},
{int32(3), `'multi':1,2,3,4,5`},
{int32(4), `'test':1A,2B,3C,4`},
{int32(5), `'word':1`},
{int32(6), `'high':16383A`},

// Text format: escaping.
{int32(7), `'don''t'`},
{int32(8), `'don''t'`},
{int32(9), `'ab\\c'`},
{int32(10), `' foo'`},

// Text format: special characters.
{int32(11), `'café' 'naïve'`},
{int32(12), `'a:b' 'c,d'`},

// Struct format.
{int32(13), `'alpha':1A 'beta':2B 'gamma'`},
{int32(14), ``},

// NULL handling.
{int32(15), nil},
{int32(16), nil},
}
require.Equal(t, expectedOutputRows, outputRows)

ensureConnValid(t, conn)
}

type clientFailSource struct {
count int
err error
Expand Down
1 change: 1 addition & 0 deletions pgconn/pgconn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ func TestConnectOAuthError(t *testing.T) {
_, err = pgconn.ConnectConfig(context.Background(), config)
require.Error(t, err, "connect should return error for invalid token")
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was caught/fixed via the linter.

func TestConnectTLSPasswordProtectedClientCertWithSSLPassword(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 2 additions & 0 deletions pgtype/pgtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ const (
RecordArrayOID = 2287
UUIDOID = 2950
UUIDArrayOID = 2951
TSVectorOID = 3614
TSVectorArrayOID = 3643
JSONBOID = 3802
JSONBArrayOID = 3807
DaterangeOID = 3912
Expand Down
3 changes: 3 additions & 0 deletions pgtype/pgtype_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func initDefaultMap() {
defaultMap.RegisterType(&Type{Name: "record", OID: RecordOID, Codec: RecordCodec{}})
defaultMap.RegisterType(&Type{Name: "text", OID: TextOID, Codec: TextCodec{}})
defaultMap.RegisterType(&Type{Name: "tid", OID: TIDOID, Codec: TIDCodec{}})
defaultMap.RegisterType(&Type{Name: "tsvector", OID: TSVectorOID, Codec: TSVectorCodec{}})
defaultMap.RegisterType(&Type{Name: "time", OID: TimeOID, Codec: TimeCodec{}})
defaultMap.RegisterType(&Type{Name: "timestamp", OID: TimestampOID, Codec: &TimestampCodec{}})
defaultMap.RegisterType(&Type{Name: "timestamptz", OID: TimestamptzOID, Codec: &TimestamptzCodec{}})
Expand Down Expand Up @@ -164,6 +165,7 @@ func initDefaultMap() {
defaultMap.RegisterType(&Type{Name: "_record", OID: RecordArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[RecordOID]}})
defaultMap.RegisterType(&Type{Name: "_text", OID: TextArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[TextOID]}})
defaultMap.RegisterType(&Type{Name: "_tid", OID: TIDArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[TIDOID]}})
defaultMap.RegisterType(&Type{Name: "_tsvector", OID: TSVectorArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[TSVectorOID]}})
defaultMap.RegisterType(&Type{Name: "_time", OID: TimeArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[TimeOID]}})
defaultMap.RegisterType(&Type{Name: "_timestamp", OID: TimestampArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[TimestampOID]}})
defaultMap.RegisterType(&Type{Name: "_timestamptz", OID: TimestamptzArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[TimestamptzOID]}})
Expand Down Expand Up @@ -242,6 +244,7 @@ func initDefaultMap() {
registerDefaultPgTypeVariants[Multirange[Range[Timestamp]]](defaultMap, "tsmultirange")
registerDefaultPgTypeVariants[Range[Timestamptz]](defaultMap, "tstzrange")
registerDefaultPgTypeVariants[Multirange[Range[Timestamptz]]](defaultMap, "tstzmultirange")
registerDefaultPgTypeVariants[TSVector](defaultMap, "tsvector")
registerDefaultPgTypeVariants[UUID](defaultMap, "uuid")

defaultMap.buildReflectTypeToType()
Expand Down
Loading