Skip to content

Commit a8336fa

Browse files
committed
Prepare overwrites existing prepared statement
Prepare now sends a Close message as part of preparing a statement. This allows changes Prepare behavior in that it is no longer an error to prepare a statement with a name that already exists. In addition, it resolves issues where the connection gets confused and doesn't realize a statement is already prepared. This could happen if a statement_timout triggered between the Parse and Describe messages. #2223
1 parent f6980e4 commit a8336fa

3 files changed

Lines changed: 35 additions & 3 deletions

File tree

conn_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,10 @@ func TestPrepareIdempotency(t *testing.T) {
503503
}
504504
}
505505

506+
// Previously, preparing a statement with the same name but different SQL would fail. Now it should succeed.
506507
_, err := conn.Prepare(context.Background(), "test", "select 'fail'::varchar")
507-
if err == nil {
508-
t.Fatalf("Prepare statement with same name but different SQL should have failed but it didn't")
508+
if err != nil {
509+
t.Fatalf("Prepare statement with same name but different SQL should have succeeded but it didn't: %v", err)
509510
return
510511
}
511512
})

pgconn/pgconn.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,7 +854,8 @@ type StatementDescription struct {
854854
}
855855

856856
// Prepare creates a prepared statement. If the name is empty, the anonymous prepared statement will be used. This
857-
// allows Prepare to also to describe statements without creating a server-side prepared statement.
857+
// allows Prepare to also to describe statements without creating a server-side prepared statement. Prepare is safe to
858+
// call with a name that is already in use. The previous statement will be closed and replaced with the new one.
858859
//
859860
// Prepare does not send a PREPARE statement to the server. It uses the PostgreSQL Parse and Describe protocol messages
860861
// directly.
@@ -874,6 +875,9 @@ func (pgConn *PgConn) Prepare(ctx context.Context, name, sql string, paramOIDs [
874875
defer pgConn.contextWatcher.Unwatch()
875876
}
876877

878+
if name != "" {
879+
pgConn.frontend.SendClose(&pgproto3.Close{ObjectType: 'S', Name: name})
880+
}
877881
pgConn.frontend.SendParse(&pgproto3.Parse{Name: name, Query: sql, ParameterOIDs: paramOIDs})
878882
pgConn.frontend.SendDescribe(&pgproto3.Describe{ObjectType: 'S', Name: name})
879883
pgConn.frontend.SendSync(&pgproto3.Sync{})

pgconn/pgconn_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,33 @@ func TestConnectConfigRequiresConfigFromParseConfig(t *testing.T) {
639639
require.PanicsWithValue(t, "config must be created by ParseConfig", func() { pgconn.ConnectConfig(ctx, config) })
640640
}
641641

642+
func TestConnPrepareOverwritesExistingPreparedStatement(t *testing.T) {
643+
t.Parallel()
644+
645+
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
646+
defer cancel()
647+
648+
pgConn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_DATABASE"))
649+
require.NoError(t, err)
650+
defer closeConn(t, pgConn)
651+
652+
psd, err := pgConn.Prepare(ctx, "ps1", "select 1", nil)
653+
require.NoError(t, err)
654+
require.NotNil(t, psd)
655+
656+
psd, err = pgConn.Prepare(ctx, "ps1", "select 2", nil)
657+
require.NoError(t, err)
658+
require.NotNil(t, psd)
659+
660+
result := pgConn.ExecPrepared(ctx, "ps1", nil, nil, nil).Read()
661+
require.NoError(t, result.Err)
662+
require.Len(t, result.Rows, 1)
663+
require.Len(t, result.Rows[0], 1)
664+
require.Equal(t, "2", string(result.Rows[0][0]))
665+
666+
ensureConnValid(t, pgConn)
667+
}
668+
642669
func TestConnPrepareSyntaxError(t *testing.T) {
643670
t.Parallel()
644671

0 commit comments

Comments
 (0)