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_ReadScalar_CanBeRetried(bool enableInternalRetries)
        {
            string sql = $"SELECT Id FROM Foo WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            transaction.EnableInternalRetries = enableInternalRetries;
            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction = transaction;
            var id = await cmd.ExecuteScalarAsync();

            Assert.Equal(1L, id);
            // Abort the transaction on the mock server. The transaction should be able to internally retry.
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            if (enableInternalRetries)
            {
                await transaction.CommitAsync();

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

                Assert.Equal(ErrorCode.Aborted, e.ErrorCode);
            }
        }
        public async Task ReadWriteTransaction_QueryWithError_AndThenNoError_FailsRetry()
        {
            string sql = $"SELECT Id FROM Foo WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Table not found: Foo"))));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction = transaction;
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                // Any query error is thrown by the first call to reader.ReadAsync();
                var e = await Assert.ThrowsAsync <SpannerException>(() => reader.ReadAsync());

                Assert.Contains("Table not found: Foo", e.InnerException.Message);
            }
            // Remove the error returned by the query on the server and abort the transaction.
            // The retry should now fail.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        public async Task ReadWriteTransaction_QueryFullyConsumed_AndThenError_FailsRetry()
        {
            string sql = $"SELECT Id FROM Foo WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction = transaction;
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id")));
                }
            }
            // Replace the result returned by the query on the server with an error and abort the transaction.
            // The retry should now fail.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Table not found: Foo"))));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
Beispiel #5
0
        public async Task TestEntity_ConvertValuesWithFloatOverflow_Succeeds()
        {
            var sql = $"SELECT `t`.`Id`, `t`.`ByteCol`, `t`.`DecimalCol`, `t`.`FloatCol`" +
                      $"{Environment.NewLine}FROM `TestEntities` AS `t`{Environment.NewLine}" +
                      $"WHERE `t`.`Id` = @__p_0{Environment.NewLine}LIMIT 1";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
                                                                new List <Tuple <V1.Type, string> >
            {
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Int64
                }, "Id"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Int64
                }, "ByteCol"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Numeric
                }, "DecimalCol"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Float64
                }, "FloatCol"),
            },
                                                                new List <object[]>
            {
                new object[] { 1L, 1L, "3.14", float.MaxValue * 2d },
            }
                                                                ));

            using var db = new TypeConversionDbContext(ConnectionString);
            var row = await db.TestEntities.FindAsync(1L);

            Assert.Equal(float.PositiveInfinity, row.FloatCol);
        }
        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);
        }
Beispiel #8
0
        public async Task TestEntity_ConvertValuesWithDecimalOverflow_Fails()
        {
            var sql = $"SELECT `t`.`Id`, `t`.`ByteCol`, `t`.`DecimalCol`, `t`.`FloatCol`" +
                      $"{Environment.NewLine}FROM `TestEntities` AS `t`{Environment.NewLine}" +
                      $"WHERE `t`.`Id` = @__p_0{Environment.NewLine}LIMIT 1";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
                                                                new List <Tuple <V1.Type, string> >
            {
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Int64
                }, "Id"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Int64
                }, "ByteCol"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Numeric
                }, "DecimalCol"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Float64
                }, "FloatCol"),
            },
                                                                new List <object[]>
            {
                new object[] { 1L, 1L, $"99999999999999999999999999999", 1d },
            }
                                                                ));

            using var db = new TypeConversionDbContext(ConnectionString);
            await Assert.ThrowsAsync <OverflowException>(() => db.TestEntities.FindAsync(1L).AsTask());
        }
        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 UpdateFailsIfVersionNumberChanged()
        {
            using var db = new MockServerVersionDbContextUsingMutations(ConnectionString);

            // Set the result of the concurrency check to an empty result set to simulate a version number that has changed.
            var concurrencySql = $"SELECT 1 FROM `SingersWithVersion` {Environment.NewLine}WHERE `SingerId` = @p0 AND `Version` = @p1";

            _fixture.SpannerMock.AddOrUpdateStatementResult(concurrencySql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "COL1"));

            // Attach a singer to the context and try to update it.
            var singer = new SingersWithVersion {
                SingerId = 1L, FirstName = "Pete", LastName = "Allison", Version = 1L
            };

            db.Attach(singer);

            singer.LastName = "Allison - Peterson";
            await Assert.ThrowsAsync <DbUpdateConcurrencyException>(() => db.SaveChangesAsync());

            // Update the concurrency check result to 1 to simulate a resolved version conflict.
            _fixture.SpannerMock.AddOrUpdateStatementResult(concurrencySql, StatementResult.CreateSelect1ResultSet());
            Assert.Equal(1L, await db.SaveChangesAsync());
        }
        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_AbortedDmlWithDifferentException_FailsRetry()
        {
            string sql = $"UPDATE Foo SET Bar='bar' WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, 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.CreateDmlCommand(sql);

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

            Assert.Equal(ErrorCode.AlreadyExists, e.ErrorCode);
            Assert.Contains("Unique key constraint violation", e.InnerException?.Message);

            // Change the error for the statement on the mock server and abort the transaction.
            // The retry should now fail as the error has changed.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Table Foo not found"))));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

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

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, 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.CreateDmlCommand(sql);

            cmd.Transaction = transaction;

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

            Assert.Equal(ErrorCode.FailedPrecondition, e.ErrorCode);
            Assert.Contains("UPDATE statement misses WHERE clause", e.InnerException?.Message);

            // Abort the transaction on the mock server. The transaction should be able to internally retry.
            _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_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 void AddOrUpdateStatementResult(string sql, StatementResult result)
 {
     _results.AddOrUpdate(sql.Trim(),
                          result,
                          (string sql, StatementResult existing) => result
                          );
 }
        public async Task ReadWriteTransaction_QueryAbortsHalfway_WithDifferentUnseenResults_CanBeRetried(bool enableInternalRetries)
        {
            string sql = $"SELECT Id FROM Foo WHERE Id IN ({_fixture.RandomLong()}, {_fixture.RandomLong()})";

            // Create a result set with 2 rows.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1, 2));
            // The following will cause the ExecuteStreamingSql method on the mock server to return an Aborted error on stream index 1 (i.e. before the row with value 2 is returned).
            // This simulates a transaction that is aborted while a streaming result set is still being returned to the client.
            var streamWritePermissions = new BlockingCollection <int>
            {
                1
            };
            var executionTime = ExecutionTime.StreamException(MockSpannerService.CreateAbortedException(sql), 1, streamWritePermissions);

            _fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(MockSpannerService.ExecuteStreamingSql), executionTime);

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

            using var transaction = await connection.BeginTransactionAsync();

            transaction.EnableInternalRetries = enableInternalRetries;
            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction  = transaction;
            using var reader = await cmd.ExecuteReaderAsync();

            // Only the first row of the reader.
            Assert.True(await reader.ReadAsync());
            Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id")));

            // Now change the result of the query, but only for the second row which has not yet been
            // seen by this transaction.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1, 3));
            // Try to get the second row of the result. This should succeed, even though the transaction
            // was aborted, retried and the reader was re-initialized under the hood. The retry succeeds
            // because only data that had not yet been seen by this transaction was changed.
            executionTime.AlwaysAllowWrite();
            if (enableInternalRetries)
            {
                Assert.True(await reader.ReadAsync());
                Assert.Equal(3, reader.GetInt64(reader.GetOrdinal("Id")));
                // Ensure that there are no more rows in the results.
                Assert.False(await reader.ReadAsync());
                // Check that the transaction really retried.
                Assert.Equal(1, transaction.RetryCount);
            }
            else
            {
                var e = await Assert.ThrowsAsync <SpannerException>(() => reader.ReadAsync());

                Assert.Equal(ErrorCode.Aborted, e.ErrorCode);
            }
        }
        private string AddSelectSingerFullNameResult(string fullName, int paramIndex)
        {
            var selectFullNameSql = $"{Environment.NewLine}SELECT `FullName`{Environment.NewLine}FROM `Singers`" +
                                    $"{Environment.NewLine}WHERE  TRUE  AND `SingerId` = @p{paramIndex}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(selectFullNameSql, StatementResult.CreateResultSet(
                                                                new List <Tuple <V1.TypeCode, string> >
            {
                Tuple.Create(V1.TypeCode.Int64, "SingerId"),
                Tuple.Create(V1.TypeCode.String, "FullName"),
            },
                                                                new List <object[]>
            {
                new object[] { 1L, fullName },
            }
                                                                ));
            return(selectFullNameSql);
        }
 private string AddFindSingerResult(string sql)
 {
     _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
                                                         new List <Tuple <V1.TypeCode, string> >
     {
         Tuple.Create(V1.TypeCode.Int64, "SingerId"),
         Tuple.Create(V1.TypeCode.Date, "BirthDate"),
         Tuple.Create(V1.TypeCode.String, "FirstName"),
         Tuple.Create(V1.TypeCode.String, "FullName"),
         Tuple.Create(V1.TypeCode.String, "LastName"),
         Tuple.Create(V1.TypeCode.Bytes, "Picture"),
     },
                                                         new List <object[]>
     {
         new object[] { 1L, null, "Alice", "Alice Morrison", "Morrison", null },
     }
                                                         ));
     return(sql);
 }
        public async Task CanInsertRowWithCommitTimestampAndComputedColumn()
        {
            using var db = new MockServerSampleDbContextUsingMutations(ConnectionString);
            var selectSql = $"{Environment.NewLine}SELECT `ColComputed`{Environment.NewLine}FROM `TableWithAllColumnTypes`{Environment.NewLine}WHERE  TRUE  AND `ColInt64` = @p0";

            _fixture.SpannerMock.AddOrUpdateStatementResult(selectSql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.String
            }, "FOO"));

            db.TableWithAllColumnTypes.Add(
                new TableWithAllColumnTypes {
                ColInt64 = 1L
            }
                );
            await db.SaveChangesAsync();

            Assert.Empty(_fixture.SpannerMock.Requests.Where(request => request is ExecuteBatchDmlRequest));
            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(request => request is ExecuteSqlRequest).Select(request => (ExecuteSqlRequest)request),
                request => Assert.Equal(selectSql.Trim(), request.Sql.Trim())
                );
            Assert.Single(_fixture.SpannerMock.Requests.Where(request => request is CommitRequest));
            // Verify the order of the requests (that is, the Select statement should be outside the implicit transaction).
            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(request => request is CommitRequest || request is ExecuteSqlRequest).Select(request => request.GetType()),
                requestType => Assert.Equal(typeof(CommitRequest), requestType),
                requestType => Assert.Equal(typeof(ExecuteSqlRequest), requestType)
                );
            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(request => request is CommitRequest).Select(request => (CommitRequest)request),
                request =>
            {
                Assert.Single(request.Mutations);
                var mutation = request.Mutations[0];
                Assert.Equal(Mutation.OperationOneofCase.Insert, mutation.OperationCase);
                Assert.Single(mutation.Insert.Values);
                var row  = mutation.Insert.Values[0];
                var cols = mutation.Insert.Columns;
                Assert.Equal("spanner.commit_timestamp()", row.Values[cols.IndexOf("ColCommitTS")].StringValue);
            }
                );
        }
        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_QueryFullyConsumed_WithModifiedResultsAfterLastRow_FailsRetry()
        {
            string sql = $"SELECT Id FROM Foo WHERE Id IN ({_fixture.RandomLong()}, {_fixture.RandomLong()})";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction = transaction;
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                while (await reader.ReadAsync())
                {
                    Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id")));
                }
            }
            // Add a row to the result of the query on the server and abort the transaction. Even though the
            // original query did not see the additional row, it did see a 'false' being returned after consuming
            // the first row in the query, meaning that it knew that there were no more results.
            // The retry should now fail.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
                                                                new List <Tuple <V1.TypeCode, string> >
            {
                Tuple.Create(V1.TypeCode.Int64, "Id"),
            },
                                                                new List <object[]>
            {
                new object[] { 1L },
                new object[] { 2L },
            }
                                                                ));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            var e = await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => transaction.CommitAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        public async Task ReadWriteTransaction_QueryHalfConsumed_WithDifferentUnseenResults_CanBeRetried(bool enableInternalRetries)
        {
            string sql = $"SELECT Id FROM Foo WHERE Id IN ({_fixture.RandomLong()}, {_fixture.RandomLong()})";

            // Create a result set with 2 rows.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1, 2));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            transaction.EnableInternalRetries = enableInternalRetries;
            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction = transaction;
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                // Only consume the first row of the reader.
                Assert.True(await reader.ReadAsync());
                Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id")));
            }
            // Change the second row of the result of the query. That row has never been seen by the transaction
            // and should therefore not cause any retry to abort.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1, 3));
            _fixture.SpannerMock.AbortTransaction(transaction.TransactionId);
            if (enableInternalRetries)
            {
                await transaction.CommitAsync();

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

                Assert.Equal(ErrorCode.Aborted, e.ErrorCode);
            }
        }
        public async Task ReadWriteTransaction_QueryAbortsHalfway_WithDifferentResults_FailsRetry()
        {
            string sql = $"SELECT Id FROM Foo WHERE Id IN ({_fixture.RandomLong()}, {_fixture.RandomLong()})";

            // Create a result set with 2 rows.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 1, 2));
            // The following will cause the ExecuteStreamingSql method on the mock server to return an Aborted error on stream index 1 (i.e. before the row with value 2 is returned).
            // This simulates a transaction that is aborted while a streaming result set is still being returned to the client.
            var streamWritePermissions = new BlockingCollection <int>
            {
                1
            };
            var executionTime = ExecutionTime.StreamException(MockSpannerService.CreateAbortedException(sql), 1, streamWritePermissions);

            _fixture.SpannerMock.AddOrUpdateExecutionTime(nameof(MockSpannerService.ExecuteStreamingSql), executionTime);

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

            using var transaction = await connection.BeginTransactionAsync();

            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction  = transaction;
            using var reader = await cmd.ExecuteReaderAsync();

            // Only the first row of the reader.
            Assert.True(await reader.ReadAsync());
            Assert.Equal(1, reader.GetInt64(reader.GetOrdinal("Id")));

            // Now change the result of the query for the record that has already been seen.
            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.Int64
            }, "Id", 3, 2));
            executionTime.AlwaysAllowWrite();
            // Try to get the second row of the result. This will now fail.
            await Assert.ThrowsAsync <SpannerAbortedDueToConcurrentModificationException>(() => reader.ReadAsync());

            Assert.Equal(1, transaction.RetryCount);
        }
        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);
        }
        public async Task ReadWriteTransaction_QueryWithSameException_CanBeRetried(bool enableInternalRetries)
        {
            string sql = $"SELECT Id FROM Foo WHERE Id={_fixture.RandomLong()}";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateException(new RpcException(new Status(StatusCode.NotFound, "Table not found: Foo"))));
            using var connection = CreateConnection();
            await connection.OpenAsync();

            using var transaction = await connection.BeginTransactionAsync();

            transaction.EnableInternalRetries = enableInternalRetries;
            var cmd = connection.CreateSelectCommand(sql);

            cmd.Transaction = transaction;
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                // Any query error is thrown by the first call to reader.ReadAsync();
                var e = await Assert.ThrowsAsync <SpannerException>(() => reader.ReadAsync());

                Assert.Equal(ErrorCode.NotFound, e.ErrorCode);
                Assert.Contains("Table not found: Foo", e.InnerException.Message);
            }
            // Abort the transaction on the mock server. The transaction should be able to internally retry.
            _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);
            }
        }
Beispiel #26
0
        public async Task TestEntity_ConvertValuesWithoutPrecisionLossOrOverflow_Succeeds()
        {
            var sql = $"SELECT `t`.`Id`, `t`.`ByteCol`, `t`.`DecimalCol`, `t`.`FloatCol`" +
                      $"{Environment.NewLine}FROM `TestEntities` AS `t`{Environment.NewLine}" +
                      $"WHERE `t`.`Id` = @__p_0{Environment.NewLine}LIMIT 1";

            _fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
                                                                new List <Tuple <V1.Type, string> >
            {
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Int64
                }, "Id"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Int64
                }, "ByteCol"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Numeric
                }, "DecimalCol"),
                new Tuple <V1.Type, string>(new V1.Type {
                    Code = V1.TypeCode.Float64
                }, "FloatCol"),
            },
                                                                new List <object[]>
            {
                new object[] { 1L, 1L, "3.14", 1.0d },
            }
                                                                ));

            using var db = new TypeConversionDbContext(ConnectionString);
            var row = await db.TestEntities.FindAsync(1L);

            Assert.Equal(1L, row.Id);
            Assert.Equal((byte)1, row.ByteCol);
            Assert.Equal(SpannerNumeric.Parse("3.14"), SpannerNumeric.FromDecimal(row.DecimalCol, LossOfPrecisionHandling.Truncate));
            Assert.Equal(1.0d, row.FloatCol);
        }
        public async Task CanUpdateCommitTimestamp()
        {
            using var db = new MockServerSampleDbContextUsingMutations(ConnectionString);
            _fixture.SpannerMock.AddOrUpdateStatementResult($"{Environment.NewLine}SELECT `ColComputed`{Environment.NewLine}FROM `TableWithAllColumnTypes`{Environment.NewLine}WHERE  TRUE  AND `ColInt64` = @p0", StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = V1.TypeCode.String
            }, "FOO"));

            var row = new TableWithAllColumnTypes {
                ColInt64 = 1L
            };

            db.TableWithAllColumnTypes.Attach(row);
            row.ColBool = true;
            await db.SaveChangesAsync();

            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(request => request is CommitRequest).Select(request => (CommitRequest)request),
                request =>
            {
                Assert.Single(request.Mutations);
                var mutation = request.Mutations[0];
                Assert.Equal(Mutation.OperationOneofCase.Update, mutation.OperationCase);
                Assert.Single(mutation.Update.Values);
                var row  = mutation.Update.Values[0];
                var cols = mutation.Update.Columns;
                Assert.Equal("spanner.commit_timestamp()", row.Values[cols.IndexOf("ColCommitTS")].StringValue);
            }
                );
        }
        public async Task VersionNumberIsAutomaticallyGeneratedOnInsertAndUpdate()
        {
            using var db = new MockServerVersionDbContextUsingMutations(ConnectionString);
            var singer = new SingersWithVersion {
                SingerId = 1L, FirstName = "Pete", LastName = "Allison"
            };

            db.Singers.Add(singer);
            await db.SaveChangesAsync();

            Assert.Empty(_fixture.SpannerMock.Requests.Where(r => r is ExecuteBatchDmlRequest));
            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(r => r is CommitRequest).Select(r => r as CommitRequest),
                r =>
            {
                Assert.Collection(
                    r.Mutations,
                    mutation =>
                {
                    Assert.Equal(Mutation.OperationOneofCase.Insert, mutation.OperationCase);
                    Assert.Equal("SingersWithVersion", mutation.Insert.Table);
                    Assert.Collection(
                        mutation.Insert.Columns,
                        column => Assert.Equal("SingerId", column),
                        column => Assert.Equal("FirstName", column),
                        column => Assert.Equal("LastName", column),
                        column => Assert.Equal("Version", column)
                        );
                    Assert.Collection(
                        mutation.Insert.Values,
                        row => Assert.Collection(
                            row.Values,
                            value => Assert.Equal("1", value.StringValue),
                            value => Assert.Equal("Pete", value.StringValue),
                            value => Assert.Equal("Allison", value.StringValue),
                            value => Assert.Equal("1", value.StringValue)
                            )
                        );
                }
                    );
            }
                );

            _fixture.SpannerMock.Reset();
            // Update the singer and verify that the version number is first checked using a SELECT statement and then is updated in a mutation.
            var concurrencySql = $"SELECT 1 FROM `SingersWithVersion` {Environment.NewLine}WHERE `SingerId` = @p0 AND `Version` = @p1";

            _fixture.SpannerMock.AddOrUpdateStatementResult(concurrencySql, StatementResult.CreateSelect1ResultSet());
            singer.LastName = "Peterson - Allison";
            await db.SaveChangesAsync();

            Assert.Empty(_fixture.SpannerMock.Requests.Where(r => r is ExecuteBatchDmlRequest));
            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(r => r is ExecuteSqlRequest).Select(r => r as ExecuteSqlRequest),
                r =>
            {
                Assert.Equal("1", r.Params.Fields["p0"].StringValue);     // SingerId
                Assert.Equal("1", r.Params.Fields["p1"].StringValue);     // Version
            }
                );
            Assert.Collection(
                _fixture.SpannerMock.Requests.Where(r => r is CommitRequest).Select(r => r as CommitRequest),
                r =>
            {
                Assert.Collection(
                    r.Mutations,
                    mutation =>
                {
                    Assert.Equal(Mutation.OperationOneofCase.Update, mutation.OperationCase);
                    Assert.Equal("SingersWithVersion", mutation.Update.Table);
                    Assert.Collection(
                        mutation.Update.Columns,
                        column => Assert.Equal("SingerId", column),
                        column => Assert.Equal("LastName", column),
                        column => Assert.Equal("Version", column)
                        );
                    Assert.Collection(
                        mutation.Update.Values,
                        row => Assert.Collection(
                            row.Values,
                            value => Assert.Equal("1", value.StringValue),
                            value => Assert.Equal("Peterson - Allison", value.StringValue),
                            value => Assert.Equal("2", value.StringValue)
                            )
                        );
                }
                    );
            }
                );
        }
 public MockSpannerServerTests(SpannerMockServerFixture service)
 {
     _fixture = service;
     // Add a simple SELECT 1 result to the mock server to be available for all test cases.
     _fixture.SpannerMock.AddOrUpdateStatementResult("SELECT 1", StatementResult.CreateSelect1ResultSet());
 }
        public async Task ReadWriteTransaction()
        {
            decimal initialBudget1 = 1225250.00m;
            decimal initialBudget2 = 2250198.28m;

            _fixture.SpannerMock.AddOrUpdateStatementResult(
                "SELECT MarketingBudget FROM Albums WHERE SingerId = 1 AND AlbumId = 1",
                StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = TypeCode.Numeric
            }, "MarketingBudget", initialBudget1));
            _fixture.SpannerMock.AddOrUpdateStatementResult(
                "SELECT MarketingBudget FROM Albums WHERE SingerId = 2 AND AlbumId = 2",
                StatementResult.CreateSingleColumnResultSet(new V1.Type {
                Code = TypeCode.Numeric
            }, "MarketingBudget", initialBudget2));
            string connectionString = $"Data Source=projects/p1/instances/i1/databases/d1;Host={_fixture.Host};Port={_fixture.Port}";

            decimal transferAmount = 200000;
            decimal secondBudget   = 0;
            decimal firstBudget    = 0;

            using var connection = new SpannerConnection(connectionString, ChannelCredentials.Insecure);
            await connection.OpenAsync();

            using (var transaction = await connection.BeginTransactionAsync())
            {
                // Create statement to select the second album's data.
                var cmdLookup = connection.CreateSelectCommand(
                    "SELECT MarketingBudget FROM Albums WHERE SingerId = 2 AND AlbumId = 2");
                cmdLookup.Transaction = transaction;
                // Excecute the select query.
                using (var reader = await cmdLookup.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        secondBudget = reader.GetNumeric(reader.GetOrdinal("MarketingBudget")).ToDecimal(LossOfPrecisionHandling.Throw);
                    }
                }
                // Read the first album's budget.
                cmdLookup = connection.CreateSelectCommand(
                    "SELECT MarketingBudget FROM Albums WHERE SingerId = 1 AND AlbumId = 1");
                cmdLookup.Transaction = transaction;
                using (var reader = await cmdLookup.ExecuteReaderAsync())
                {
                    while (await reader.ReadAsync())
                    {
                        firstBudget = reader.GetNumeric(reader.GetOrdinal("MarketingBudget")).ToDecimal(LossOfPrecisionHandling.Throw);
                    }
                }
                // Specify update command parameters.
                var cmd = connection.CreateUpdateCommand("Albums",
                                                         new SpannerParameterCollection
                {
                    { "SingerId", SpannerDbType.Int64 },
                    { "AlbumId", SpannerDbType.Int64 },
                    { "MarketingBudget", SpannerDbType.Numeric },
                });
                cmd.Transaction = transaction;
                // Update second album to remove the transfer amount.
                secondBudget -= transferAmount;
                cmd.Parameters["SingerId"].Value        = 2;
                cmd.Parameters["AlbumId"].Value         = 2;
                cmd.Parameters["MarketingBudget"].Value = secondBudget;
                await cmd.ExecuteNonQueryAsync();

                // Update first album to add the transfer amount.
                firstBudget += transferAmount;
                cmd.Parameters["SingerId"].Value        = 1;
                cmd.Parameters["AlbumId"].Value         = 1;
                cmd.Parameters["MarketingBudget"].Value = firstBudget;
                await cmd.ExecuteNonQueryAsync();

                await transaction.CommitAsync();
            }
            // Assert that the correct updates were sent.
            Stack <IMessage> requests = new Stack <IMessage>(_fixture.SpannerMock.Requests);

            Assert.Equal(typeof(CommitRequest), requests.Peek().GetType());
            CommitRequest commit = (CommitRequest)requests.Pop();

            Assert.Equal(2, commit.Mutations.Count);

            Mutation update1 = commit.Mutations.Last();

            Assert.Equal(Mutation.OperationOneofCase.Update, update1.OperationCase);
            Assert.Equal("Albums", update1.Update.Table);
            Assert.Equal("1", update1.Update.Values.ElementAt(0).Values.ElementAt(0).StringValue);
            Assert.Equal(
                SpannerNumeric.FromDecimal(initialBudget1 + transferAmount, LossOfPrecisionHandling.Throw),
                SpannerNumeric.Parse(update1.Update.Values.ElementAt(0).Values.ElementAt(2).StringValue));

            Mutation update2 = commit.Mutations.First();

            Assert.Equal(Mutation.OperationOneofCase.Update, update2.OperationCase);
            Assert.Equal("Albums", update2.Update.Table);
            Assert.Equal("2", update2.Update.Values.ElementAt(0).Values.ElementAt(0).StringValue);
            Assert.Equal(
                SpannerNumeric.FromDecimal(initialBudget2 - transferAmount, LossOfPrecisionHandling.Throw),
                SpannerNumeric.Parse(update2.Update.Values.ElementAt(0).Values.ElementAt(2).StringValue));
        }