This is an automated email from the ASF dual-hosted git repository.
thunguo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go-samples.git
The following commit(s) were added to refs/heads/main by this push:
new 8c7214f test(at): add MySQL batch integration tests (#107)
8c7214f is described below
commit 8c7214f476f83a16622fd2c5911aa8d85bdc6ee0
Author: Mochimia <[email protected]>
AuthorDate: Thu Sep 3 20:08:35 2026 +0800
test(at): add MySQL batch integration tests (#107)
* test(at): add MySQL batch integration tests
* test(at): handle polling errors and simplify MySQL DSN
---
integrate_test/at/batch/Makefile | 21 +++
integrate_test/at/batch/caller_owned.go | 131 +++++++++++++
integrate_test/at/batch/main.go | 94 ++++++++++
integrate_test/at/batch/managed.go | 320 ++++++++++++++++++++++++++++++++
start_integrate_test.sh | 1 +
5 files changed, 567 insertions(+)
diff --git a/integrate_test/at/batch/Makefile b/integrate_test/at/batch/Makefile
new file mode 100644
index 0000000..ad125e5
--- /dev/null
+++ b/integrate_test/at/batch/Makefile
@@ -0,0 +1,21 @@
+# 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.
+
+
+
+run:
+ go run $(DIRECTORY)/*.go
diff --git a/integrate_test/at/batch/caller_owned.go
b/integrate_test/at/batch/caller_owned.go
new file mode 100644
index 0000000..be3797f
--- /dev/null
+++ b/integrate_test/at/batch/caller_owned.go
@@ -0,0 +1,131 @@
+/*
+ * 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 main
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ seataSQL "seata.apache.org/seata-go/v2/pkg/datasource/sql"
+ "seata.apache.org/seata-go/v2/pkg/tm"
+)
+
+var callerOwnedIDs = [3]int64{93001, 93002, 93003}
+
+var callerOwnedBaseline = []orderRow{
+ {ID: 93001, UserID: "batch-caller-1", CommodityCode: "batch", Count:
10, Money: 100, Descs: "caller baseline 1"},
+ {ID: 93002, UserID: "batch-caller-2", CommodityCode: "batch", Count:
20, Money: 200, Descs: "caller baseline 2"},
+ {ID: 93003, UserID: "batch-caller-3", CommodityCode: "batch", Count:
30, Money: 300, Descs: "caller baseline 3"},
+}
+
+var triggerGlobalRollback = errors.New("trigger expected global rollback")
+
+func (s batchSuite) runCallerOwnedGlobalRollback(ctx context.Context) error {
+ if err := s.restoreFixture(ctx, callerOwnedIDs, callerOwnedBaseline);
err != nil {
+ return fmt.Errorf("restore fixture: %w", err)
+ }
+
+ var xid string
+ globalErr := tm.WithGlobalTx(ctx, &tm.GtxConfig{
+ Name: "ATSemanticBatchCallerOwnedGlobalRollback",
+ Timeout: caseTimeout,
+ }, func(txCtx context.Context) error {
+ xid = tm.GetXID(txCtx)
+ if xid == "" {
+ return fmt.Errorf("global transaction xid is empty")
+ }
+
+ tx, err := s.db.BeginTx(txCtx, nil)
+ if err != nil {
+ return fmt.Errorf("begin caller-owned transaction: %w",
err)
+ }
+ committed := false
+ defer func() {
+ if !committed {
+ _ = tx.Rollback()
+ }
+ }()
+
+ result, err := seataSQL.ExecBatchInTxContext(txCtx, tx,
+ "DELETE FROM order_tbl WHERE id = ?",
+ [][]any{{int64(93001)}, {int64(93002)}, {int64(93003)}})
+ if err != nil {
+ return fmt.Errorf("execute batch: %w", err)
+ }
+ if err := assertSuccessfulBatch(result,
seataSQL.BatchTransactionPending); err != nil {
+ return err
+ }
+ if err := assertRows(txCtx, tx, callerOwnedIDs, []orderRow{});
err != nil {
+ return fmt.Errorf("same transaction was not usable
after batch: %w", err)
+ }
+ if err := tx.Commit(); err != nil {
+ return fmt.Errorf("caller commit: %w", err)
+ }
+ committed = true
+
+ if err := assertRows(txCtx, s.db, callerOwnedIDs,
[]orderRow{}); err != nil {
+ return fmt.Errorf("verify locally committed deletes:
%w", err)
+ }
+ if err := s.assertUndoLogCount(txCtx, xid, 1); err != nil {
+ return fmt.Errorf("verify single branch undo log: %w",
err)
+ }
+ return triggerGlobalRollback
+ })
+ if globalErr == nil {
+ return fmt.Errorf("expected global rollback trigger error")
+ }
+ if !errors.Is(globalErr, triggerGlobalRollback) {
+ return fmt.Errorf("global rollback failed: %w", globalErr)
+ }
+
+ if err := waitForRows(ctx, s.db, callerOwnedIDs, callerOwnedBaseline);
err != nil {
+ return fmt.Errorf("verify rows restored by global rollback:
%w", err)
+ }
+ if err := s.waitForUndoLogCleanup(ctx, xid); err != nil {
+ return err
+ }
+ return nil
+}
+
+func waitForRows(ctx context.Context, queryer rowQueryer, ids [3]int64,
expected []orderRow) error {
+ deadline := time.NewTimer(pollTimeout)
+ defer deadline.Stop()
+ ticker := time.NewTicker(100 * time.Millisecond)
+ defer ticker.Stop()
+
+ var lastErr error
+ for {
+ err := assertRows(ctx, queryer, ids, expected)
+ if err == nil {
+ return nil
+ }
+ if !errors.Is(err, errRowsMismatch) {
+ return err
+ }
+ lastErr = err
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-deadline.C:
+ return fmt.Errorf("rows did not reach expected state
within %s: %w", pollTimeout, lastErr)
+ case <-ticker.C:
+ }
+ }
+}
diff --git a/integrate_test/at/batch/main.go b/integrate_test/at/batch/main.go
new file mode 100644
index 0000000..d6bc26c
--- /dev/null
+++ b/integrate_test/at/batch/main.go
@@ -0,0 +1,94 @@
+/*
+ * 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 main
+
+import (
+ "context"
+ "database/sql"
+ "log"
+ "net"
+ "os"
+ "time"
+
+ mysqlDriver "github.com/go-sql-driver/mysql"
+ "seata.apache.org/seata-go/v2/pkg/client"
+ seataSQL "seata.apache.org/seata-go/v2/pkg/datasource/sql"
+)
+
+const (
+ caseTimeout = 30 * time.Second
+ pollTimeout = 15 * time.Second
+)
+
+type batchSuite struct {
+ db *sql.DB
+}
+
+func main() {
+ client.InitPath("./conf/seatago.yml")
+
+ db, err := sql.Open(seataSQL.SeataATMySQLDriver, mysqlDSN())
+ if err != nil {
+ log.Fatalf("open database: %v", err)
+ }
+ defer db.Close()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ defer cancel()
+ if err := db.PingContext(ctx); err != nil {
+ log.Fatalf("ping database: %v", err)
+ }
+
+ suite := batchSuite{db: db}
+ if err := suite.runManagedSuccess(ctx); err != nil {
+ log.Fatalf("managed success: %v", err)
+ }
+ if err := suite.runManagedPartialFailure(ctx); err != nil {
+ log.Fatalf("managed partial failure: %v", err)
+ }
+ if err := suite.runCallerOwnedGlobalRollback(ctx); err != nil {
+ log.Fatalf("caller-owned global rollback: %v", err)
+ }
+ log.Println("AT semantic batch integration case passed")
+}
+
+func mysqlDSN() string {
+ if dsn := os.Getenv("MYSQL_DSN"); dsn != "" {
+ return dsn
+ }
+
+ password := os.Getenv("MYSQL_PASSWORD")
+ if password == "" {
+ password = envOrDefault("MYSQL_ROOT_PASSWORD", "12345678")
+ }
+ config := mysqlDriver.NewConfig()
+ config.User = envOrDefault("MYSQL_USERNAME", "root")
+ config.Passwd = password
+ config.Net = "tcp"
+ config.Addr = net.JoinHostPort(envOrDefault("MYSQL_HOST", "127.0.0.1"),
envOrDefault("MYSQL_PORT", "3306"))
+ config.DBName = envOrDefault("MYSQL_DB", "seata_client")
+ config.InterpolateParams = true
+ return config.FormatDSN()
+}
+
+func envOrDefault(name, fallback string) string {
+ if value := os.Getenv(name); value != "" {
+ return value
+ }
+ return fallback
+}
diff --git a/integrate_test/at/batch/managed.go
b/integrate_test/at/batch/managed.go
new file mode 100644
index 0000000..40c6bec
--- /dev/null
+++ b/integrate_test/at/batch/managed.go
@@ -0,0 +1,320 @@
+/*
+ * 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 main
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "fmt"
+ "reflect"
+ "time"
+
+ mysqlDriver "github.com/go-sql-driver/mysql"
+ seataSQL "seata.apache.org/seata-go/v2/pkg/datasource/sql"
+ "seata.apache.org/seata-go/v2/pkg/tm"
+)
+
+var (
+ errRowsMismatch = errors.New("rows do not match expected state")
+ errUndoLogCountMismatch = errors.New("undo log count does not match
expected state")
+)
+
+var managedSuccessIDs = [3]int64{91001, 91002, 91003}
+
+var managedSuccessBaseline = []orderRow{
+ {ID: 91001, UserID: "batch-managed-1", CommodityCode: "batch", Count:
10, Money: 100, Descs: "managed baseline 1"},
+ {ID: 91002, UserID: "batch-managed-2", CommodityCode: "batch", Count:
20, Money: 200, Descs: "managed baseline 2"},
+ {ID: 91003, UserID: "batch-managed-3", CommodityCode: "batch", Count:
30, Money: 300, Descs: "managed baseline 3"},
+}
+
+type orderRow struct {
+ ID int64
+ UserID string
+ CommodityCode string
+ Count int64
+ Money int64
+ Descs string
+}
+
+var managedFailureIDs = [3]int64{92001, 92002, 92003}
+
+var managedFailureBaseline = []orderRow{
+ {ID: 92002, UserID: "batch-existing", CommodityCode: "batch", Count:
20, Money: 200, Descs: "duplicate baseline"},
+}
+
+type rowQueryer interface {
+ QueryContext(ctx context.Context, query string, args ...any)
(*sql.Rows, error)
+}
+
+func (s batchSuite) runManagedSuccess(ctx context.Context) error {
+ if err := s.restoreFixture(ctx, managedSuccessIDs,
managedSuccessBaseline); err != nil {
+ return fmt.Errorf("restore fixture: %w", err)
+ }
+
+ expected := append([]orderRow(nil), managedSuccessBaseline...)
+ expected[0].Count, expected[0].Descs = 11, "managed updated 1"
+ expected[1].Count, expected[1].Descs = 22, "managed updated 2"
+ expected[2].Count, expected[2].Descs = 33, "managed updated 3"
+
+ var xid string
+ err := tm.WithGlobalTx(ctx, &tm.GtxConfig{
+ Name: "ATSemanticBatchManagedSuccess",
+ Timeout: caseTimeout,
+ }, func(txCtx context.Context) error {
+ xid = tm.GetXID(txCtx)
+ if xid == "" {
+ return fmt.Errorf("global transaction xid is empty")
+ }
+
+ result, err := seataSQL.ExecBatchContext(txCtx, s.db,
+ "UPDATE order_tbl SET count = ?, descs = ? WHERE id =
?",
+ [][]any{
+ {int64(11), "managed updated 1", int64(91001)},
+ {int64(22), "managed updated 2", int64(91002)},
+ {int64(33), "managed updated 3", int64(91003)},
+ })
+ if err != nil {
+ return fmt.Errorf("execute batch: %w", err)
+ }
+ if err := assertSuccessfulBatch(result,
seataSQL.BatchTransactionCommitted); err != nil {
+ return err
+ }
+ if err := assertRows(txCtx, s.db, managedSuccessIDs, expected);
err != nil {
+ return fmt.Errorf("verify locally committed rows: %w",
err)
+ }
+ if err := s.assertUndoLogCount(txCtx, xid, 1); err != nil {
+ return fmt.Errorf("verify single branch undo log: %w",
err)
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("global transaction did not commit: %w", err)
+ }
+
+ if err := assertRows(ctx, s.db, managedSuccessIDs, expected); err !=
nil {
+ return fmt.Errorf("verify globally committed rows: %w", err)
+ }
+ if err := s.waitForUndoLogCleanup(ctx, xid); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (s batchSuite) runManagedPartialFailure(ctx context.Context) error {
+ if err := s.restoreFixture(ctx, managedFailureIDs,
managedFailureBaseline); err != nil {
+ return fmt.Errorf("restore fixture: %w", err)
+ }
+
+ var xid string
+ err := tm.WithGlobalTx(ctx, &tm.GtxConfig{
+ Name: "ATSemanticBatchManagedPartialFailure",
+ Timeout: caseTimeout,
+ }, func(txCtx context.Context) error {
+ xid = tm.GetXID(txCtx)
+ if xid == "" {
+ return fmt.Errorf("global transaction xid is empty")
+ }
+
+ result, batchErr := seataSQL.ExecBatchContext(txCtx, s.db,
+ "INSERT INTO order_tbl (id, user_id, commodity_code,
count, money, descs) VALUES (?, ?, ?, ?, ?, ?)",
+ [][]any{
+ {int64(92001), "batch-inserted-1", "batch",
int64(10), int64(100), "inserted before failure"},
+ {int64(92002), "batch-duplicate", "batch",
int64(20), int64(200), "deterministic duplicate"},
+ {int64(92003), "batch-not-executed", "batch",
int64(30), int64(300), "must not execute"},
+ })
+ if batchErr == nil {
+ return fmt.Errorf("expected duplicate-key batch
failure")
+ }
+ var mysqlErr *mysqlDriver.MySQLError
+ if !errors.As(batchErr, &mysqlErr) || mysqlErr.Number != 1062 {
+ return fmt.Errorf("expected MySQL duplicate-key error
1062, got %w", batchErr)
+ }
+ if err := assertPartialFailureBatch(result); err != nil {
+ return err
+ }
+ if err := assertRows(txCtx, s.db, managedFailureIDs,
managedFailureBaseline); err != nil {
+ return fmt.Errorf("verify automatic local rollback:
%w", err)
+ }
+ if err := s.assertUndoLogCount(txCtx, xid, 0); err != nil {
+ return fmt.Errorf("verify failed local transaction did
not register a branch: %w", err)
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("enclosing global transaction did not commit
after handled batch failure: %w", err)
+ }
+
+ if err := assertRows(ctx, s.db, managedFailureIDs,
managedFailureBaseline); err != nil {
+ return fmt.Errorf("verify rows after enclosing global commit:
%w", err)
+ }
+ return s.assertUndoLogCount(ctx, xid, 0)
+}
+
+func assertSuccessfulBatch(result seataSQL.BatchResult, transactionState
seataSQL.BatchTransactionState) error {
+ if len(result.Items) != 3 {
+ return fmt.Errorf("expected 3 batch items, got %d",
len(result.Items))
+ }
+ if result.Outcome.FailedIndex != seataSQL.NoFailedBatchItem {
+ return fmt.Errorf("expected failed index %d, got %d",
seataSQL.NoFailedBatchItem, result.Outcome.FailedIndex)
+ }
+ if result.Outcome.FailurePhase != seataSQL.BatchPhaseNone {
+ return fmt.Errorf("expected no failure phase, got %d",
result.Outcome.FailurePhase)
+ }
+ if result.Outcome.TransactionState != transactionState {
+ return fmt.Errorf("expected transaction state %d, got %d",
transactionState, result.Outcome.TransactionState)
+ }
+ for index, item := range result.Items {
+ if item.Index != index {
+ return fmt.Errorf("item %d reported index %d", index,
item.Index)
+ }
+ if item.State != seataSQL.BatchItemExecuted {
+ return fmt.Errorf("item %d expected executed state, got
%d", index, item.State)
+ }
+ rowsAffected, err := item.RowsAffected()
+ if err != nil {
+ return fmt.Errorf("item %d rows affected: %w", index,
err)
+ }
+ if rowsAffected != 1 {
+ return fmt.Errorf("item %d expected 1 affected row, got
%d", index, rowsAffected)
+ }
+ }
+ return nil
+}
+
+func assertPartialFailureBatch(result seataSQL.BatchResult) error {
+ if len(result.Items) != 3 {
+ return fmt.Errorf("expected 3 batch items, got %d",
len(result.Items))
+ }
+ if result.Outcome.FailedIndex != 1 {
+ return fmt.Errorf("expected failed index 1, got %d",
result.Outcome.FailedIndex)
+ }
+ if result.Outcome.FailurePhase != seataSQL.BatchPhaseExecute {
+ return fmt.Errorf("expected execute failure phase, got %d",
result.Outcome.FailurePhase)
+ }
+ if result.Outcome.TransactionState !=
seataSQL.BatchTransactionRolledBack {
+ return fmt.Errorf("expected rolled-back transaction state, got
%d", result.Outcome.TransactionState)
+ }
+ expectedStates := []seataSQL.BatchItemState{
+ seataSQL.BatchItemExecuted,
+ seataSQL.BatchItemFailed,
+ seataSQL.BatchItemNotExecuted,
+ }
+ for index, item := range result.Items {
+ if item.Index != index {
+ return fmt.Errorf("item %d reported index %d", index,
item.Index)
+ }
+ if item.State != expectedStates[index] {
+ return fmt.Errorf("item %d expected state %d, got %d",
index, expectedStates[index], item.State)
+ }
+ }
+ if result.Items[1].Err() == nil {
+ return fmt.Errorf("failed item did not preserve its execution
error")
+ }
+ rowsAffected, err := result.Items[0].RowsAffected()
+ if err != nil {
+ return fmt.Errorf("first item rows affected: %w", err)
+ }
+ if rowsAffected != 1 {
+ return fmt.Errorf("first item expected 1 affected row, got %d",
rowsAffected)
+ }
+ return nil
+}
+
+func (s batchSuite) restoreFixture(ctx context.Context, ids [3]int64, expected
[]orderRow) error {
+ tx, err := s.db.BeginTx(ctx, nil)
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ if _, err := tx.ExecContext(ctx, "DELETE FROM order_tbl WHERE id IN (?,
?, ?)", ids[0], ids[1], ids[2]); err != nil {
+ return err
+ }
+ for _, row := range expected {
+ if _, err := tx.ExecContext(ctx,
+ "INSERT INTO order_tbl (id, user_id, commodity_code,
count, money, descs) VALUES (?, ?, ?, ?, ?, ?)",
+ row.ID, row.UserID, row.CommodityCode, row.Count,
row.Money, row.Descs); err != nil {
+ return err
+ }
+ }
+ return tx.Commit()
+}
+
+func assertRows(ctx context.Context, queryer rowQueryer, ids [3]int64,
expected []orderRow) error {
+ rows, err := queryer.QueryContext(ctx,
+ "SELECT id, user_id, commodity_code, count, money, descs FROM
order_tbl WHERE id IN (?, ?, ?) ORDER BY id",
+ ids[0], ids[1], ids[2])
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ actual := make([]orderRow, 0, len(expected))
+ for rows.Next() {
+ var row orderRow
+ if err := rows.Scan(&row.ID, &row.UserID, &row.CommodityCode,
&row.Count, &row.Money, &row.Descs); err != nil {
+ return err
+ }
+ actual = append(actual, row)
+ }
+ if err := rows.Err(); err != nil {
+ return err
+ }
+ if !reflect.DeepEqual(actual, expected) {
+ return fmt.Errorf("%w: expected rows %+v, got %+v",
errRowsMismatch, expected, actual)
+ }
+ return nil
+}
+
+func (s batchSuite) assertUndoLogCount(ctx context.Context, xid string,
expected int) error {
+ var count int
+ if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM undo_log
WHERE xid = ?", xid).Scan(&count); err != nil {
+ return err
+ }
+ if count != expected {
+ return fmt.Errorf("%w: expected %d undo-log rows for xid %s,
got %d", errUndoLogCountMismatch, expected, xid, count)
+ }
+ return nil
+}
+
+func (s batchSuite) waitForUndoLogCleanup(ctx context.Context, xid string)
error {
+ deadline := time.NewTimer(pollTimeout)
+ defer deadline.Stop()
+ ticker := time.NewTicker(100 * time.Millisecond)
+ defer ticker.Stop()
+
+ var lastErr error
+ for {
+ err := s.assertUndoLogCount(ctx, xid, 0)
+ if err == nil {
+ return nil
+ }
+ if !errors.Is(err, errUndoLogCountMismatch) {
+ return err
+ }
+ lastErr = err
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-deadline.C:
+ return fmt.Errorf("undo log for xid %s was not cleaned
within %s: %w", xid, pollTimeout, lastErr)
+ case <-ticker.C:
+ }
+ }
+}
diff --git a/start_integrate_test.sh b/start_integrate_test.sh
index 87e926b..b057aab 100755
--- a/start_integrate_test.sh
+++ b/start_integrate_test.sh
@@ -19,6 +19,7 @@
array+=("integrate_test/at/insert")
array+=("integrate_test/at/insert_on_update")
array+=("integrate_test/at/select_for_update")
+array+=("integrate_test/at/batch")
array+=("integrate_test/tcc/insert")
array+=("integrate_test/tcc/insert_on_update")
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]