public async Task ReadWriteTransaction_Retry_GivesUp()
        {
            string sql = $"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            transaction.MaxInternalRetryCount = 3;

            for (var i = 0; i < transaction.MaxInternalRetryCount; i++)
            {
                _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
                var cmd = connection.CreateDmlCommand(sql);
                cmd.Transaction = transaction;
                var updateCount = await cmd.ExecuteNonQueryAsync();

                Assert.Equal(1, updateCount);
                Assert.Equal(i + 1, transaction.RetryCount);
            }
            // The next statement that aborts will cause the transaction to fail.
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            var cmd2 = connection.CreateDmlCommand(sql);

            cmd2.Transaction = transaction;
            var e = await Assert.ThrowsAsync <SpannerException>(() => cmd2.ExecuteNonQueryAsync());

            Assert.Equal(ErrorCode.Aborted, e.ErrorCode);
            Assert.Contains("Transaction was aborted because it aborted and retried too many times", e.Message);
        }
        public async Task ReadWriteTransaction_BatchDmlWithSameExceptionHalfwayAndDifferentResults_FailsRetry()
        {
            string sql1 = $"UPDATE Foo SET Bar='valid' WHERE Id={_fixture.RandomLong()}";
            string sql2 = $"UPDATE Foo SET Bar='invalid' Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateUpdateCount(1));
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql2, StatementResult.CreateException(new RpcException(new Status(StatusCode.FailedPrecondition, "UPDATE statement misses WHERE clause"))));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateBatchDmlCommand();

            cmd.Transaction = transaction;
            cmd.Add(sql1);
            cmd.Add(sql2);
            var e = await Assert.ThrowsAsync <SpannerBatchNonQueryException>(() => cmd.ExecuteNonQueryAsync());

            Assert.Contains("UPDATE statement misses WHERE clause", e.Message);
            Assert.Equal(new List <long> {
                1
            }, e.SuccessfulCommandResults);

            // Change the result of the first statement and abort the transaction.
            // The retry should now fail, even though the error code and message is the same.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateUpdateCount(2));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        public async Task ReadWriteTransaction_AbortedDml_IsAutomaticallyRetried(bool enableInternalRetries)
        {
            string sql = $"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            transaction.EnableInternalRetries = enableInternalRetries;
            // Abort the transaction on the mock server. The transaction should be able to internally retry.
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            var cmd = connection.CreateDmlCommand(sql);

            cmd.Transaction = transaction;
            if (enableInternalRetries)
            {
                var updateCount = await cmd.ExecuteNonQueryAsync();

                Assert.Equal(1, updateCount);
                Assert.Equal(1, transaction.RetryCount);
            }
            else
            {
                var e = await Assert.ThrowsAsync <SpannerException>(() => cmd.ExecuteNonQueryAsync());

                Assert.Equal(ErrorCode.Aborted, e.ErrorCode);
            }
        }
        public async Task ReadWriteTransaction_BatchDmlWithDifferentException_FailsRetry()
        {
            string sql1 = $"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateException(new RpcException(new Status(StatusCode.AlreadyExists, "Unique key constraint violation"))));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateBatchDmlCommand();

            cmd.Transaction = transaction;
            cmd.Add(sql1);
            try
            {
                await cmd.ExecuteNonQueryAsync();

                Assert.True(false, "Missing expected exception");
            }
            catch (SpannerException e) when(e.ErrorCode == ErrorCode.AlreadyExists)
            {
                Assert.Contains("Unique key constraint violation", e.InnerException?.Message);
            }

            // Remove the error message for the udpate statement.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateUpdateCount(1));
            // Abort the transaction and try to commit. That will trigger a retry, but the retry
            // will not receive an error for the update statement. That will fail the retry.
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);

            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        public async Task ReadWriteTransaction_ModifiedBatchDmlUpdateCount_FailsRetry()
        {
            string sql1 = $"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}";
            string sql2 = $"UPDATE Foo SET Bar='baz' WHERE Id IN ({_fixture.RandomLong()},{_fixture.RandomLong()})";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateUpdateCount(1));
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql2, StatementResult.CreateUpdateCount(2));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateBatchDmlCommand();

            cmd.Transaction = transaction;
            cmd.Add(sql1);
            cmd.Add(sql2);
            Assert.Equal(new List <long> {
                1, 2
            }, await cmd.ExecuteNonQueryAsync());
            // Change the update count returned by one of the statements and abort the transaction.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql2, StatementResult.CreateUpdateCount(1));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);

            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        public async Task ReadWriteTransaction_ModifiedDmlUpdateCount_FailsRetry()
        {
            // This statement returns an update count of 1 the first time.
            string sql = $"UPDATE Foo SET Bar='baz' WHERE Id IN ({_fixture.RandomLong()},{_fixture.RandomLong()})";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1));

            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            // Execute an update and then change the return value for the statement before the retry is executed.
            var cmd = connection.CreateDmlCommand(sql);

            cmd.Transaction = transaction;
            Assert.Equal(1, await cmd.ExecuteNonQueryAsync());
            // The update statement will return 2 the next time it is executed.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(2));

            // Now abort the transaction and try to execute another DML statement. The retry will fail because it sees
            // a different update count during the retry.
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            cmd             = connection.CreateDmlCommand($"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}");
            cmd.Transaction = transaction;
            var e = await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => cmd.ExecuteNonQueryAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        public async Task ReadWriteTransaction_BatchDmlWithSameExceptionHalfwayAndSameResults_CanBeRetried(bool enableInternalRetries)
        {
            string sql1 = $"UPDATE Foo SET Bar='valid' WHERE Id={_fixture.RandomLong()}";
            string sql2 = $"UPDATE Foo SET Bar='invalid' Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql1, StatementResult.CreateUpdateCount(1));
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql2, StatementResult.CreateException(new RpcException(new Status(StatusCode.FailedPrecondition, "UPDATE statement misses WHERE clause"))));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            transaction.EnableInternalRetries = enableInternalRetries;
            var cmd = connection.CreateBatchDmlCommand();

            cmd.Transaction = transaction;
            cmd.Add(sql1);
            cmd.Add(sql2);

            var e = await Assert.ThrowsAsync <SpannerBatchNonQueryException>(() => cmd.ExecuteNonQueryAsync());

            Assert.Contains("UPDATE statement misses WHERE clause", e.Message);
            Assert.Equal(new List <long> {
                1
            }, e.SuccessfulCommandResults);

            // Abort the transaction and try to commit. That will trigger a retry, and the retry will receive
            // the same error and the same results for the BatchDML call as the original attempt.
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            if (enableInternalRetries)
            {
                await transaction.CommitAsync();

                Assert.Equal(1, transaction.RetryCount);
            }
            else
            {
                var se = await Assert.ThrowsAsync <SpannerException>(() => transaction.CommitAsync());

                Assert.Equal(ErrorCode.Aborted, se.ErrorCode);
            }
        }
        public async Task ReadWriteTransaction_WithoutAbort_DoesNotRetry()
        {
            string sql = $"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateUpdateCount(1));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateDmlCommand(sql);

            cmd.Transaction = transaction;
            var updateCount = await cmd.ExecuteNonQueryAsync();

            await transaction.CommitAsync();

            Assert.Equal(1, updateCount);
            Assert.Equal(0, transaction.RetryCount);
        }