This is an automated email from the ASF dual-hosted git repository.

tew pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git


The following commit(s) were added to refs/heads/master by this push:
     new 2cce81bc feat: add QueryContext for AT mode (#1039)
2cce81bc is described below

commit 2cce81bc00446d0849c12f6f0742829eaae72138
Author: simple-xair <[email protected]>
AuthorDate: Sat Mar 14 19:35:38 2026 +0800

    feat: add QueryContext for AT mode (#1039)
    
    * feat: add QueryContext for AT mode
    
    * refactor: split createNewTxOnExec
    
    * fix: add missing /v2 suffix in import paths
    
    ---------
    
    Co-authored-by: lxfeng1997 <[email protected]>
---
 pkg/datasource/sql/conn.go                         |  14 +-
 pkg/datasource/sql/conn_at.go                      | 121 +++++++++-
 .../sql/exec/select_for_update_executor.go         |   4 +-
 pkg/datasource/sql/rows_commit_on_close.go         |  91 +++++++
 pkg/datasource/sql/rows_commit_on_close_test.go    | 265 +++++++++++++++++++++
 pkg/datasource/sql/types/types.go                  |   2 +
 6 files changed, 470 insertions(+), 27 deletions(-)

diff --git a/pkg/datasource/sql/conn.go b/pkg/datasource/sql/conn.go
index 7a57c829..f40e8153 100644
--- a/pkg/datasource/sql/conn.go
+++ b/pkg/datasource/sql/conn.go
@@ -114,11 +114,7 @@ func (c *Conn) Exec(query string, args []driver.Value) 
(driver.Result, error) {
 func (c *Conn) ExecContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Result, error) {
        targetConn, ok := c.targetConn.(driver.ExecerContext)
        if !ok {
-               values := make([]driver.Value, 0, len(args))
-               for i := range args {
-                       values = append(values, args[i].Value)
-               }
-               return c.Exec(query, values)
+               return c.Exec(query, util.NamedValueToValue(args))
        }
 
        ret, err := targetConn.ExecContext(ctx, query, args)
@@ -166,13 +162,7 @@ func (c *Conn) Query(query string, args []driver.Value) 
(driver.Rows, error) {
 func (c *Conn) QueryContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Rows, error) {
        conn, ok := c.targetConn.(driver.QueryerContext)
        if !ok {
-               values := make([]driver.Value, 0, len(args))
-
-               for i := range args {
-                       values = append(values, args[i].Value)
-               }
-
-               return c.Query(query, values)
+               return c.Query(query, util.NamedValueToValue(args))
        }
 
        ret, err := conn.QueryContext(ctx, query, args)
diff --git a/pkg/datasource/sql/conn_at.go b/pkg/datasource/sql/conn_at.go
index 576d22f4..e19aa017 100644
--- a/pkg/datasource/sql/conn_at.go
+++ b/pkg/datasource/sql/conn_at.go
@@ -51,7 +51,7 @@ func (c *ATConn) ExecContext(ctx context.Context, query 
string, args []driver.Na
                }()
        }
 
-       ret, err := c.createNewTxOnExecIfNeed(ctx, func() (types.ExecResult, 
error) {
+       ret, err := c.createTxAndExecIfNeeded(ctx, func() (types.ExecResult, 
error) {
                executor, err := exec.BuildExecutor(c.res.dbType, 
c.txCtx.TransactionMode, query)
                if err != nil {
                        return nil, err
@@ -85,6 +85,48 @@ func (c *ATConn) ExecContext(ctx context.Context, query 
string, args []driver.Na
        return ret.GetResult(), nil
 }
 
+// QueryContext
+func (c *ATConn) QueryContext(ctx context.Context, query string, args 
[]driver.NamedValue) (driver.Rows, error) {
+       if c.createOnceTxContext(ctx) {
+               defer func() {
+                       c.txCtx = types.NewTxCtx()
+               }()
+       }
+
+       ret, err := c.createTxAndQueryIfNeeded(ctx, func() (types.ExecResult, 
error) {
+               executor, err := exec.BuildExecutor(c.res.dbType, 
c.txCtx.TransactionMode, query)
+               if err != nil {
+                       return nil, err
+               }
+
+               execCtx := &types.ExecContext{
+                       TxCtx:                c.txCtx,
+                       Query:                query,
+                       NamedValues:          args,
+                       Conn:                 c.targetConn,
+                       DBName:               c.dbName,
+                       DbVersion:            c.GetDbVersion(),
+                       IsSupportsSavepoints: true,
+                       IsAutoCommit:         c.GetAutoCommit(),
+               }
+
+               ret, err := executor.ExecWithNamedValue(ctx, execCtx,
+                       func(ctx context.Context, query string, args 
[]driver.NamedValue) (types.ExecResult, error) {
+                               ret, err := c.Conn.QueryContext(ctx, query, 
args)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               return types.NewResult(types.WithRows(ret)), nil
+                       })
+
+               return ret, err
+       })
+       if err != nil {
+               return nil, err
+       }
+       return ret.GetRows(), nil
+}
+
 // BeginTx
 func (c *ATConn) BeginTx(ctx context.Context, opts driver.TxOptions) 
(driver.Tx, error) {
        c.autoCommit = false
@@ -122,7 +164,8 @@ func (c *ATConn) createOnceTxContext(ctx context.Context) 
bool {
        return onceTx
 }
 
-func (c *ATConn) createNewTxOnExecIfNeed(ctx context.Context, f func() 
(types.ExecResult, error)) (types.ExecResult, error) {
+// createTxAndExecIfNeeded creates a transaction for execution context and 
commits it after execution
+func (c *ATConn) createTxAndExecIfNeeded(ctx context.Context, f func() 
(types.ExecResult, error)) (types.ExecResult, error) {
        var (
                tx  driver.Tx
                err error
@@ -133,25 +176,26 @@ func (c *ATConn) createNewTxOnExecIfNeed(ctx 
context.Context, f func() (types.Ex
                if err != nil {
                        return nil, err
                }
-       }
-       defer func() {
-               recoverErr := recover()
-               if recoverErr != nil {
-                       log.Errorf("at exec panic, recoverErr:%v", recoverErr)
-                       if tx != nil {
-                               rollbackErr := tx.Rollback()
-                               if rollbackErr != nil {
-                                       log.Errorf("conn at rollback error:%v", 
rollbackErr)
+               defer func() {
+                       recoverErr := recover()
+                       if recoverErr != nil {
+                               log.Errorf("at exec panic, recoverErr:%v", 
recoverErr)
+                               if tx != nil {
+                                       rollbackErr := tx.Rollback()
+                                       if rollbackErr != nil {
+                                               log.Errorf("conn at rollback 
error:%v", rollbackErr)
+                                       }
                                }
                        }
-               }
-       }()
+               }()
+       }
 
        ret, err := f()
        if err != nil {
                return nil, err
        }
 
+       // For ExecContext, commit the transaction if it was created
        if tx != nil {
                if err := tx.Commit(); err != nil {
                        return nil, err
@@ -160,3 +204,54 @@ func (c *ATConn) createNewTxOnExecIfNeed(ctx 
context.Context, f func() (types.Ex
 
        return ret, nil
 }
+
+// createTxAndQueryIfNeeded creates a transaction for query context and wraps 
the rows to commit on close
+func (c *ATConn) createTxAndQueryIfNeeded(ctx context.Context, f func() 
(types.ExecResult, error)) (types.ExecResult, error) {
+       var (
+               tx  driver.Tx
+               err error
+       )
+
+       if c.txCtx.TransactionMode != types.Local && tm.IsGlobalTx(ctx) && 
c.autoCommit {
+               tx, err = c.BeginTx(ctx, driver.TxOptions{Isolation: 
driver.IsolationLevel(gosql.LevelDefault)})
+               if err != nil {
+                       return nil, err
+               }
+               defer func() {
+                       recoverErr := recover()
+                       if recoverErr != nil {
+                               log.Errorf("at exec panic, recoverErr:%v", 
recoverErr)
+                               if tx != nil {
+                                       rollbackErr := tx.Rollback()
+                                       if rollbackErr != nil {
+                                               log.Errorf("conn at rollback 
error:%v", rollbackErr)
+                                       }
+                               }
+                       }
+               }()
+       }
+
+       ret, err := f()
+       if err != nil {
+               return nil, err
+       }
+
+       // For QueryContext, wrap rows to commit on close
+       var activeTx driver.Tx
+       if c.txCtx.LocalTx != nil {
+               activeTx = c.txCtx.LocalTx
+       } else if tx != nil {
+               activeTx = tx
+       }
+
+       if activeTx != nil {
+               if rows, ok := ret.(types.ExecResult); ok {
+                       if dr := rows.GetRows(); dr != nil {
+                               wrappedRows := &RowsCommitOnClose{rows: dr, tx: 
activeTx}
+                               return 
types.NewResult(types.WithRows(wrappedRows)), nil
+                       }
+               }
+       }
+
+       return ret, nil
+}
diff --git a/pkg/datasource/sql/exec/select_for_update_executor.go 
b/pkg/datasource/sql/exec/select_for_update_executor.go
index 30f4086c..fd82b2fa 100644
--- a/pkg/datasource/sql/exec/select_for_update_executor.go
+++ b/pkg/datasource/sql/exec/select_for_update_executor.go
@@ -268,8 +268,8 @@ func (s SelectForUpdateExecutor) ExecWithValue(ctx 
context.Context, execCtx *typ
        }
 
        if originalAutoCommit {
-               if err = tx.Commit(); err != nil {
-                       return nil, err
+               if tx != nil {
+                       execCtx.TxCtx.LocalTx = tx
                }
                execCtx.IsAutoCommit = true
        }
diff --git a/pkg/datasource/sql/rows_commit_on_close.go 
b/pkg/datasource/sql/rows_commit_on_close.go
new file mode 100644
index 00000000..0e20b78b
--- /dev/null
+++ b/pkg/datasource/sql/rows_commit_on_close.go
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sql
+
+import (
+       "database/sql/driver"
+       "errors"
+       "io"
+       "sync/atomic"
+
+       "seata.apache.org/seata-go/v2/pkg/util/log"
+)
+
+type closeState int32
+
+const (
+       stateOpen closeState = iota
+       stateClosing
+       stateClosed
+)
+
+// RowsCommitOnClose wraps driver.Rows and commits the transaction
+type RowsCommitOnClose struct {
+       rows driver.Rows
+       tx   driver.Tx
+
+       state int32 // atomic closeState
+       err   error
+}
+
+// Close implements driver.Rows.Close.
+func (r *RowsCommitOnClose) Close() error {
+       if !atomic.CompareAndSwapInt32(&r.state, int32(stateOpen), 
int32(stateClosing)) {
+               return r.err
+       }
+
+       var rowErr error
+       if r.rows != nil {
+               rowErr = r.rows.Close()
+       }
+
+       var txErr error
+       if r.tx != nil {
+               txErr = r.tx.Commit()
+               if txErr != nil {
+                       log.Errorf("RowsCommitOnClose: commit failed: %v", 
txErr)
+               }
+               r.tx = nil
+       }
+
+       var errs []error
+       if rowErr != nil {
+               errs = append(errs, rowErr)
+       }
+       if txErr != nil {
+               errs = append(errs, txErr)
+       }
+       r.err = errors.Join(errs...)
+
+       atomic.StoreInt32(&r.state, int32(stateClosed))
+       return r.err
+}
+
+// Columns implements driver.Rows.Columns.
+func (r *RowsCommitOnClose) Columns() []string {
+       return r.rows.Columns()
+}
+
+// Next implements driver.Rows.Next.
+func (r *RowsCommitOnClose) Next(dest []driver.Value) error {
+       err := r.rows.Next(dest)
+       if err == io.EOF {
+               _ = r.Close()
+       }
+       return err
+}
diff --git a/pkg/datasource/sql/rows_commit_on_close_test.go 
b/pkg/datasource/sql/rows_commit_on_close_test.go
new file mode 100644
index 00000000..e49f5995
--- /dev/null
+++ b/pkg/datasource/sql/rows_commit_on_close_test.go
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package sql
+
+import (
+       "database/sql/driver"
+       "errors"
+       "io"
+       "testing"
+
+       "github.com/golang/mock/gomock"
+       "github.com/stretchr/testify/assert"
+
+       "seata.apache.org/seata-go/v2/pkg/datasource/sql/mock"
+)
+
+/*
+ * ------------------------------------------------------------------------
+ * Close()
+ * ------------------------------------------------------------------------
+ */
+
+func TestRowsCommitOnClose_Close(t *testing.T) {
+       tests := []struct {
+               name      string
+               setup     func(ctrl *gomock.Controller) *RowsCommitOnClose
+               wantErr   error
+               wantState int32
+       }{
+               {
+                       name: "rows and tx success",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               rows := mock.NewMockTestDriverRows(ctrl)
+                               tx := mock.NewMockTestDriverTx(ctrl)
+                               rows.EXPECT().Close().Return(nil)
+                               tx.EXPECT().Commit().Return(nil)
+
+                               return &RowsCommitOnClose{
+                                       rows:  rows,
+                                       tx:    tx,
+                                       state: int32(stateOpen),
+                               }
+                       },
+                       wantErr:   nil,
+                       wantState: int32(stateClosed),
+               },
+               {
+                       name: "rows close error only",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               rows := mock.NewMockTestDriverRows(ctrl)
+                               tx := mock.NewMockTestDriverTx(ctrl)
+                               rowErr := errors.New("close failed")
+
+                               rows.EXPECT().Close().Return(rowErr)
+                               tx.EXPECT().Commit().Return(nil)
+
+                               return &RowsCommitOnClose{
+                                       rows:  rows,
+                                       tx:    tx,
+                                       state: int32(stateOpen),
+                               }
+                       },
+                       wantErr:   errors.New("close failed"),
+                       wantState: int32(stateClosed),
+               },
+               {
+                       name: "tx commit error only",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               rows := mock.NewMockTestDriverRows(ctrl)
+                               tx := mock.NewMockTestDriverTx(ctrl)
+                               txErr := errors.New("commit failed")
+
+                               rows.EXPECT().Close().Return(nil)
+                               tx.EXPECT().Commit().Return(txErr)
+
+                               return &RowsCommitOnClose{
+                                       rows:  rows,
+                                       tx:    tx,
+                                       state: int32(stateOpen),
+                               }
+                       },
+                       wantErr:   errors.New("commit failed"),
+                       wantState: int32(stateClosed),
+               },
+               {
+                       name: "rows and tx both error",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               rows := mock.NewMockTestDriverRows(ctrl)
+                               tx := mock.NewMockTestDriverTx(ctrl)
+                               rowErr := errors.New("close failed")
+                               txErr := errors.New("commit failed")
+
+                               rows.EXPECT().Close().Return(rowErr)
+                               tx.EXPECT().Commit().Return(txErr)
+
+                               return &RowsCommitOnClose{
+                                       rows:  rows,
+                                       tx:    tx,
+                                       state: int32(stateOpen),
+                               }
+                       },
+                       wantErr:   errors.New("close failed\ncommit failed"),
+                       wantState: int32(stateClosed),
+               },
+               {
+                       name: "already closed",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               return &RowsCommitOnClose{
+                                       state: int32(stateClosed),
+                               }
+                       },
+                       wantErr:   nil,
+                       wantState: int32(stateClosed),
+               },
+               {
+                       name: "closing in progress",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               return &RowsCommitOnClose{
+                                       state: int32(stateClosing),
+                               }
+                       },
+                       wantErr:   nil,
+                       wantState: int32(stateClosing),
+               },
+               {
+                       name: "nil rows",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               tx := mock.NewMockTestDriverTx(ctrl)
+                               tx.EXPECT().Commit().Return(nil)
+                               return &RowsCommitOnClose{
+                                       tx:    tx,
+                                       state: int32(stateOpen),
+                               }
+                       },
+                       wantErr:   nil,
+                       wantState: int32(stateClosed),
+               },
+               {
+                       name: "nil tx",
+                       setup: func(ctrl *gomock.Controller) *RowsCommitOnClose 
{
+                               rows := mock.NewMockTestDriverRows(ctrl)
+                               rows.EXPECT().Close().Return(nil)
+                               return &RowsCommitOnClose{
+                                       rows:  rows,
+                                       state: int32(stateOpen),
+                               }
+                       },
+                       wantErr:   nil,
+                       wantState: int32(stateClosed),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       ctrl := gomock.NewController(t)
+                       defer ctrl.Finish()
+
+                       r := tt.setup(ctrl)
+                       err := r.Close()
+
+                       if tt.wantErr == nil {
+                               assert.NoError(t, err)
+                       } else {
+                               assert.Error(t, err)
+                               assert.Equal(t, tt.wantErr.Error(), err.Error())
+                       }
+
+                       assert.Equal(t, tt.wantState, r.state)
+                       if r.state == int32(stateClosed) {
+                               assert.Nil(t, r.tx)
+                       }
+               })
+       }
+}
+
+/*
+ * ------------------------------------------------------------------------
+ * Columns()
+ * ------------------------------------------------------------------------
+ */
+
+func TestRowsCommitOnClose_Columns_WithRows(t *testing.T) {
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       rowsMock := mock.NewMockTestDriverRows(ctrl)
+       rowsMock.EXPECT().Columns().Return([]string{"id", "name"})
+
+       r := &RowsCommitOnClose{rows: rowsMock}
+       cols := r.Columns()
+
+       assert.Equal(t, []string{"id", "name"}, cols)
+}
+
+func TestRowsCommitOnClose_Columns_NilRows(t *testing.T) {
+       r := &RowsCommitOnClose{}
+       assert.Panics(t, func() {
+               _ = r.Columns()
+       })
+}
+
+/*
+ * ------------------------------------------------------------------------
+ * Next()
+ * ------------------------------------------------------------------------
+ */
+
+func TestRowsCommitOnClose_Next_Error(t *testing.T) {
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       rowsMock := mock.NewMockTestDriverRows(ctrl)
+       dest := make([]driver.Value, 1)
+       rowsMock.EXPECT().Next(dest).Return(errors.New("next error"))
+
+       r := &RowsCommitOnClose{rows: rowsMock}
+       err := r.Next(dest)
+
+       assert.EqualError(t, err, "next error")
+}
+
+func TestRowsCommitOnClose_Next_NilRows(t *testing.T) {
+       r := &RowsCommitOnClose{}
+       assert.Panics(t, func() {
+               _ = r.Next(make([]driver.Value, 1))
+       })
+}
+
+func TestRowsCommitOnClose_Next_EOF_Close(t *testing.T) {
+       ctrl := gomock.NewController(t)
+       defer ctrl.Finish()
+
+       rowsMock := mock.NewMockTestDriverRows(ctrl)
+       txMock := mock.NewMockTestDriverTx(ctrl)
+
+       rowsMock.EXPECT().Next(gomock.Any()).Return(io.EOF)
+       rowsMock.EXPECT().Close().Return(nil)
+       txMock.EXPECT().Commit().Return(nil)
+
+       r := &RowsCommitOnClose{
+               rows:  rowsMock,
+               tx:    txMock,
+               state: int32(stateOpen),
+       }
+
+       err := r.Next(make([]driver.Value, 1))
+       assert.Equal(t, io.EOF, err)
+       assert.Equal(t, int32(stateClosed), r.state)
+       assert.Nil(t, r.tx)
+}
diff --git a/pkg/datasource/sql/types/types.go 
b/pkg/datasource/sql/types/types.go
index 127bb925..d85cea32 100644
--- a/pkg/datasource/sql/types/types.go
+++ b/pkg/datasource/sql/types/types.go
@@ -143,6 +143,8 @@ type TransactionContext struct {
        GlobalLockRequire bool
        // RoundImages when run in AT mode, record before and after Row image
        RoundImages *RoundRecordImage
+       // LocalTx local transaction instance, managed by at connection
+       LocalTx driver.Tx
 }
 
 // ExecContext


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to