Skip to content

Commit 969bf52

Browse files
authored
feat: add client side statement parser (#38)
* feat: add client side statement parser * feat: statement parser * feat: support PDML * add tests for DML batches * test: add more tests and documentation * chore: rename Dml and Ddl to DML and DDL * fix: add json file as string variable * chore: remove unused file
1 parent 2d698b7 commit 969bf52

13 files changed

+2152
-42
lines changed

aborted_transactions_test.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,44 @@ func TestUpdateAborted(t *testing.T) {
119119
}
120120
}
121121

122+
func TestBatchUpdateAborted(t *testing.T) {
123+
t.Parallel()
124+
125+
db, server, teardown := setupTestDBConnection(t)
126+
defer teardown()
127+
128+
ctx := context.Background()
129+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
130+
if err != nil {
131+
t.Fatalf("begin failed: %v", err)
132+
}
133+
server.TestSpanner.PutExecutionTime(testutil.MethodExecuteBatchDml, testutil.SimulatedExecutionTime{
134+
Errors: []error{status.Error(codes.Aborted, "Aborted")},
135+
})
136+
if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil {
137+
t.Fatalf("start batch failed: %v", err)
138+
}
139+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
140+
t.Fatalf("update failed: %v", err)
141+
}
142+
if _, err := tx.ExecContext(ctx, "RUN BATCH"); err != nil {
143+
t.Fatalf("run batch failed: %v", err)
144+
}
145+
err = tx.Commit()
146+
if err != nil {
147+
t.Fatalf("commit failed: %v", err)
148+
}
149+
reqs := drainRequestsFromServer(server.TestSpanner)
150+
execReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.ExecuteBatchDmlRequest{}))
151+
if g, w := len(execReqs), 2; g != w {
152+
t.Fatalf("batch request count mismatch\nGot: %v\nWant: %v", g, w)
153+
}
154+
commitReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.CommitRequest{}))
155+
if g, w := len(commitReqs), 1; g != w {
156+
t.Fatalf("commit request count mismatch\nGot: %v\nWant: %v", g, w)
157+
}
158+
}
159+
122160
func TestQueryAborted(t *testing.T) {
123161
testRetryReadWriteTransactionWithQueryWithRetrySuccess(t, func(server testutil.InMemSpannerServer) {
124162
server.PutExecutionTime(testutil.MethodExecuteStreamingSql, testutil.SimulatedExecutionTime{
@@ -577,6 +615,58 @@ func TestSecondUpdateAborted(t *testing.T) {
577615
}
578616
}
579617

618+
func TestSecondBatchUpdateAborted(t *testing.T) {
619+
t.Parallel()
620+
621+
db, server, teardown := setupTestDBConnection(t)
622+
defer teardown()
623+
624+
ctx := context.Background()
625+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
626+
if err != nil {
627+
t.Fatalf("begin failed: %v", err)
628+
}
629+
if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil {
630+
t.Fatalf("failed to start batch: %v", err)
631+
}
632+
if _, err := tx.ExecContext(ctx, testutil.UpdateSingersSetLastName); err != nil {
633+
t.Fatalf("update singers failed: %v", err)
634+
}
635+
if _, err := tx.ExecContext(ctx, "RUN BATCH"); err != nil {
636+
t.Fatalf("failed to run batch: %v", err)
637+
}
638+
639+
server.TestSpanner.PutExecutionTime(testutil.MethodExecuteBatchDml, testutil.SimulatedExecutionTime{
640+
Errors: []error{status.Error(codes.Aborted, "Aborted")},
641+
})
642+
if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil {
643+
t.Fatalf("failed to start batch: %v", err)
644+
}
645+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
646+
t.Fatalf("update bar failed: %v", err)
647+
}
648+
// This statement will return Aborted, the transaction will be retried internally and the statement is
649+
// then executed once more and should return the correct value.
650+
if _, err := tx.ExecContext(ctx, "RUN BATCH"); err != nil {
651+
t.Fatalf("failed to run batch: %v", err)
652+
}
653+
654+
if err := tx.Commit(); err != nil {
655+
t.Fatalf("commit failed: %v", err)
656+
}
657+
reqs := drainRequestsFromServer(server.TestSpanner)
658+
execReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.ExecuteBatchDmlRequest{}))
659+
// The server should receive 4 batch statements, as each update statement should
660+
// be executed twice.
661+
if g, w := len(execReqs), 4; g != w {
662+
t.Fatalf("batch request count mismatch\nGot: %v\nWant: %v", g, w)
663+
}
664+
commitReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.CommitRequest{}))
665+
if g, w := len(commitReqs), 1; g != w {
666+
t.Fatalf("commit request count mismatch\nGot: %v\nWant: %v", g, w)
667+
}
668+
}
669+
580670
func TestSecondUpdateAborted_FirstStatementWithSameError(t *testing.T) {
581671
t.Parallel()
582672

@@ -707,6 +797,173 @@ func testSecondUpdateAborted_FirstResultChanged(t *testing.T, firstResult *testu
707797
}
708798
}
709799

800+
func TestBatchUpdateAbortedWithError(t *testing.T) {
801+
t.Parallel()
802+
803+
db, server, teardown := setupTestDBConnection(t)
804+
defer teardown()
805+
806+
// Make sure that one of the DML statements will return an error.
807+
server.TestSpanner.PutStatementResult(testutil.UpdateSingersSetLastName, &testutil.StatementResult{
808+
Type: testutil.StatementResultError,
809+
Err: status.Error(codes.NotFound, "Table not found"),
810+
})
811+
812+
ctx := context.Background()
813+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
814+
if err != nil {
815+
t.Fatalf("begin failed: %v", err)
816+
}
817+
if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil {
818+
t.Fatalf("failed to start batch: %v", err)
819+
}
820+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
821+
t.Fatalf("dml statement failed: %v", err)
822+
}
823+
// This statement should fail with NotFound when the batch is executed.
824+
// That will also be the result during the retry. Note that the error
825+
// will not be returned now, but when the batch is executed.
826+
if _, err = tx.ExecContext(ctx, testutil.UpdateSingersSetLastName); err != nil {
827+
t.Fatalf("dml statement failed: %v", err)
828+
}
829+
// Note that even though Spanner returns the row count for a Batch DML that
830+
// fails halfway, go/sql does not do that, so result will be nil for batch
831+
// statements that fail. Internally, the driver still keeps track of the row
832+
// counts that were returned, and uses that in the retry strategy.
833+
if _, err := tx.ExecContext(ctx, "RUN BATCH"); spanner.ErrCode(err) != codes.NotFound {
834+
t.Fatalf("error code mismatch\nGot: %v\nWant: %v", spanner.ErrCode(err), codes.NotFound)
835+
}
836+
837+
// Abort the transaction. The internal retry should succeed as teh same error
838+
// and the same row count is returned during the retry.
839+
server.TestSpanner.PutExecutionTime(testutil.MethodCommitTransaction, testutil.SimulatedExecutionTime{
840+
Errors: []error{status.Error(codes.Aborted, "Aborted")},
841+
})
842+
err = tx.Commit()
843+
if err != nil {
844+
t.Fatalf("commit failed: %v", err)
845+
}
846+
reqs := drainRequestsFromServer(server.TestSpanner)
847+
execReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.ExecuteBatchDmlRequest{}))
848+
if g, w := len(execReqs), 2; g != w {
849+
t.Fatalf("batch request count mismatch\nGot: %v\nWant: %v", g, w)
850+
}
851+
commitReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.CommitRequest{}))
852+
if g, w := len(commitReqs), 2; g != w {
853+
t.Fatalf("commit request count mismatch\nGot: %v\nWant: %v", g, w)
854+
}
855+
}
856+
857+
func TestBatchUpdateAbortedWithError_DifferentRowCountDuringRetry(t *testing.T) {
858+
t.Parallel()
859+
860+
db, server, teardown := setupTestDBConnection(t)
861+
defer teardown()
862+
863+
// Make sure that one of the DML statements will return an error.
864+
server.TestSpanner.PutStatementResult(testutil.UpdateSingersSetLastName, &testutil.StatementResult{
865+
Type: testutil.StatementResultError,
866+
Err: status.Error(codes.NotFound, "Table not found"),
867+
})
868+
869+
ctx := context.Background()
870+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
871+
if err != nil {
872+
t.Fatalf("begin failed: %v", err)
873+
}
874+
if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil {
875+
t.Fatalf("failed to start batch: %v", err)
876+
}
877+
if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil {
878+
t.Fatalf("dml statement failed: %v", err)
879+
}
880+
// This statement should fail with NotFound when the batch is executed.
881+
// That will also be the result during the retry. Note that the error
882+
// will not be returned now, but when the batch is executed.
883+
if _, err = tx.ExecContext(ctx, testutil.UpdateSingersSetLastName); err != nil {
884+
t.Fatalf("dml statement failed: %v", err)
885+
}
886+
if _, err := tx.ExecContext(ctx, "RUN BATCH"); spanner.ErrCode(err) != codes.NotFound {
887+
t.Fatalf("error code mismatch\nGot: %v\nWant: %v", spanner.ErrCode(err), codes.NotFound)
888+
}
889+
890+
// Change the returned row count of the first DML statement. This will cause
891+
// the retry to fail.
892+
server.TestSpanner.PutStatementResult(testutil.UpdateBarSetFoo, &testutil.StatementResult{
893+
Type: testutil.StatementResultUpdateCount,
894+
UpdateCount: testutil.UpdateBarSetFooRowCount + 1,
895+
})
896+
server.TestSpanner.PutExecutionTime(testutil.MethodCommitTransaction, testutil.SimulatedExecutionTime{
897+
Errors: []error{status.Error(codes.Aborted, "Aborted")},
898+
})
899+
err = tx.Commit()
900+
if err != ErrAbortedDueToConcurrentModification {
901+
t.Fatalf("commit error mismatch\nGot: %v\nWant: %v", err, ErrAbortedDueToConcurrentModification)
902+
}
903+
reqs := drainRequestsFromServer(server.TestSpanner)
904+
execReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.ExecuteBatchDmlRequest{}))
905+
if g, w := len(execReqs), 2; g != w {
906+
t.Fatalf("batch request count mismatch\nGot: %v\nWant: %v", g, w)
907+
}
908+
commitReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.CommitRequest{}))
909+
// The commit should be attempted only once.
910+
if g, w := len(commitReqs), 1; g != w {
911+
t.Fatalf("commit request count mismatch\nGot: %v\nWant: %v", g, w)
912+
}
913+
}
914+
915+
func TestBatchUpdateAbortedWithError_DifferentErrorDuringRetry(t *testing.T) {
916+
t.Parallel()
917+
918+
db, server, teardown := setupTestDBConnection(t)
919+
defer teardown()
920+
921+
// Make sure that one of the DML statements will return an error.
922+
server.TestSpanner.PutStatementResult(testutil.UpdateSingersSetLastName, &testutil.StatementResult{
923+
Type: testutil.StatementResultError,
924+
Err: status.Error(codes.NotFound, "Table not found"),
925+
})
926+
927+
ctx := context.Background()
928+
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
929+
if err != nil {
930+
t.Fatalf("begin failed: %v", err)
931+
}
932+
if _, err := tx.ExecContext(ctx, "START BATCH DML"); err != nil {
933+
t.Fatalf("failed to start batch: %v", err)
934+
}
935+
if _, err = tx.ExecContext(ctx, testutil.UpdateSingersSetLastName); err != nil {
936+
t.Fatalf("dml statement failed: %v", err)
937+
}
938+
if _, err := tx.ExecContext(ctx, "RUN BATCH"); spanner.ErrCode(err) != codes.NotFound {
939+
t.Fatalf("error code mismatch\nGot: %v\nWant: %v", spanner.ErrCode(err), codes.NotFound)
940+
}
941+
942+
// Remove the error for the DML statement and cause a retry. The missing
943+
// error for the DML statement should fail the retry.
944+
server.TestSpanner.PutStatementResult(testutil.UpdateSingersSetLastName, &testutil.StatementResult{
945+
Type: testutil.StatementResultUpdateCount,
946+
UpdateCount: testutil.UpdateSingersSetLastNameRowCount,
947+
})
948+
server.TestSpanner.PutExecutionTime(testutil.MethodCommitTransaction, testutil.SimulatedExecutionTime{
949+
Errors: []error{status.Error(codes.Aborted, "Aborted")},
950+
})
951+
err = tx.Commit()
952+
if err != ErrAbortedDueToConcurrentModification {
953+
t.Fatalf("commit error mismatch\nGot: %v\nWant: %v", err, ErrAbortedDueToConcurrentModification)
954+
}
955+
reqs := drainRequestsFromServer(server.TestSpanner)
956+
execReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.ExecuteBatchDmlRequest{}))
957+
if g, w := len(execReqs), 2; g != w {
958+
t.Fatalf("batch request count mismatch\nGot: %v\nWant: %v", g, w)
959+
}
960+
commitReqs := requestsOfType(reqs, reflect.TypeOf(&sppb.CommitRequest{}))
961+
// The commit should be attempted only once.
962+
if g, w := len(commitReqs), 1; g != w {
963+
t.Fatalf("commit request count mismatch\nGot: %v\nWant: %v", g, w)
964+
}
965+
}
966+
710967
func firstNonZero(values ...int) int {
711968
for _, v := range values {
712969
if v > 0 {

0 commit comments

Comments
 (0)