Skip to content

Commit b4c26d1

Browse files
EDsCODEclaude
andcommitted
Handle nested BEGIN with warning instead of error
Match PostgreSQL behavior for nested BEGIN transactions. PostgreSQL issues a warning "there is already a transaction in progress" and continues, while DuckDB throws an error. Changes: - Add writeNoticeResponse() for sending non-fatal notices/warnings - Add sendNotice() helper method on clientConn - Intercept BEGIN when already in transaction and send warning - Return success (CommandComplete) instead of passing to DuckDB - Handle in both simple query and extended query protocols This fixes compatibility with clients like dbt that may issue multiple BEGIN statements. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent db4b0d4 commit b4c26d1

File tree

3 files changed

+75
-0
lines changed

3 files changed

+75
-0
lines changed

server/conn.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,16 @@ func (c *clientConn) handleQuery(body []byte) error {
292292

293293
// For non-SELECT queries, use Exec
294294
if cmdType != "SELECT" {
295+
// Handle nested BEGIN: PostgreSQL issues a warning but continues,
296+
// while DuckDB throws an error. Match PostgreSQL behavior.
297+
if cmdType == "BEGIN" && c.txStatus == txStatusTransaction {
298+
c.sendNotice("WARNING", "25001", "there is already a transaction in progress")
299+
writeCommandComplete(c.writer, "BEGIN")
300+
writeReadyForQuery(c.writer, c.txStatus)
301+
c.writer.Flush()
302+
return nil
303+
}
304+
295305
result, err := c.db.Exec(query)
296306
if err != nil {
297307
c.sendError("ERROR", "42000", err.Error())
@@ -902,6 +912,11 @@ func (c *clientConn) sendError(severity, code, message string) {
902912
c.writer.Flush()
903913
}
904914

915+
func (c *clientConn) sendNotice(severity, code, message string) {
916+
writeNoticeResponse(c.writer, severity, code, message)
917+
// Don't flush here - let the caller decide when to flush
918+
}
919+
905920
// Extended query protocol handlers
906921

907922
func (c *clientConn) handleParse(body []byte) {
@@ -1228,6 +1243,14 @@ func (c *clientConn) handleExecute(body []byte) {
12281243
log.Printf("[%s] Execute %q with %d params: %s", c.username, portalName, len(args), p.stmt.query)
12291244

12301245
if cmdType != "SELECT" {
1246+
// Handle nested BEGIN: PostgreSQL issues a warning but continues,
1247+
// while DuckDB throws an error. Match PostgreSQL behavior.
1248+
if cmdType == "BEGIN" && c.txStatus == txStatusTransaction {
1249+
c.sendNotice("WARNING", "25001", "there is already a transaction in progress")
1250+
writeCommandComplete(c.writer, "BEGIN")
1251+
return
1252+
}
1253+
12311254
// Non-SELECT: use Exec with converted query
12321255
result, err := c.db.Exec(p.stmt.convertedQuery, args...)
12331256
if err != nil {

server/conn_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,29 @@ func TestTransactionErrorStatus(t *testing.T) {
332332
t.Errorf("after ROLLBACK from error txStatus = %c, want %c", c.txStatus, txStatusIdle)
333333
}
334334
}
335+
336+
func TestNestedBeginDetection(t *testing.T) {
337+
// Test that we can detect when a nested BEGIN would occur
338+
// The actual warning is sent in handleQuery, but we test the detection logic here
339+
c := &clientConn{txStatus: txStatusIdle}
340+
341+
// First BEGIN should work normally
342+
c.updateTxStatus("BEGIN")
343+
if c.txStatus != txStatusTransaction {
344+
t.Errorf("after first BEGIN txStatus = %c, want %c", c.txStatus, txStatusTransaction)
345+
}
346+
347+
// At this point, a second BEGIN should trigger warning behavior
348+
// In handleQuery, when cmdType == "BEGIN" && c.txStatus == txStatusTransaction,
349+
// we send a warning and return success without calling DuckDB
350+
isNestedBegin := c.txStatus == txStatusTransaction
351+
if !isNestedBegin {
352+
t.Error("expected nested BEGIN to be detected")
353+
}
354+
355+
// Transaction status should remain 'T' (not change to 'I' or 'E')
356+
// The warning is sent but the transaction continues
357+
if c.txStatus != txStatusTransaction {
358+
t.Errorf("txStatus should still be %c after nested BEGIN detection, got %c", txStatusTransaction, c.txStatus)
359+
}
360+
}

server/protocol.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,32 @@ func writeErrorResponse(w io.Writer, severity, code, message string) error {
223223
return writeMessage(w, msgErrorResponse, data)
224224
}
225225

226+
// writeNoticeResponse sends a notice/warning to the client
227+
// Unlike errors, notices are informational and don't terminate the command
228+
func writeNoticeResponse(w io.Writer, severity, code, message string) error {
229+
var data []byte
230+
231+
// Severity (WARNING, NOTICE, INFO, DEBUG, LOG)
232+
data = append(data, 'S')
233+
data = append(data, []byte(severity)...)
234+
data = append(data, 0)
235+
236+
// SQLSTATE code
237+
data = append(data, 'C')
238+
data = append(data, []byte(code)...)
239+
data = append(data, 0)
240+
241+
// Message
242+
data = append(data, 'M')
243+
data = append(data, []byte(message)...)
244+
data = append(data, 0)
245+
246+
// Terminator
247+
data = append(data, 0)
248+
249+
return writeMessage(w, msgNoticeResponse, data)
250+
}
251+
226252
// writeCommandComplete sends a command completion message
227253
func writeCommandComplete(w io.Writer, tag string) error {
228254
data := []byte(tag)

0 commit comments

Comments
 (0)