Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 89 additions & 42 deletions test/EFCore.Cosmos.FunctionalTests/CosmosTransactionalBatchTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,24 @@ public virtual async Task SaveChanges_too_large_entry_after_smaller_throws_after
Assert.Equal("1", (await assertContext.Customers.FirstAsync()).Id);
}

[ConditionalFact]
public virtual async Task SaveChanges_transaction_behaviour_always_payload_exactly_2_mib()
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();

using var context = contextFactory.CreateContext();
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;

context.Customers.Add(new Customer { Id = "1", Name = new string('x', 1048291), PartitionKey = "1" });
context.Customers.Add(new Customer { Id = "2", Name = new string('x', 1048291), PartitionKey = "1" });

await context.SaveChangesAsync();

using var assertContext = contextFactory.CreateContext();
var customersCount = await assertContext.Customers.CountAsync();
Assert.Equal(2, customersCount);
}

[ConditionalFact]
public virtual async Task SaveChanges_transaction_behaviour_always_payload_larger_than_cosmos_limit_throws()
{
Expand All @@ -381,80 +399,106 @@ public virtual async Task SaveChanges_transaction_behaviour_always_payload_large
Assert.Equal(0, customersCount);
}

// The tests below will fail if the cosmos db sdk is updated and the serialization logic for transactional batches has changed
private const int nameLengthToExceed2MiBWithSpecialCharIdOnUpdate = 1046358;

[ConditionalTheory, InlineData(true), InlineData(false)]
public virtual async Task SaveChanges_transaction_behaviour_always_single_entity_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over(bool oneByteOver)
public virtual async Task SaveChanges_update_id_contains_special_chars_which_makes_request_larger_than_2_mib_splits_into_2_batches(bool isIdSpecialChar)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();

using var context = contextFactory.CreateContext();
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;

var customer = new Customer { Id = new string('x', 1_000), PartitionKey = new string('x', 1_000) };
var id1 = isIdSpecialChar ? new string('€', 341) : new string('x', 341);
var id2 = isIdSpecialChar ? new string('Ω', 341) : new string('y', 341);

var customer1 = new Customer { Id = id1, PartitionKey = new string('€', 341) };
var customer2 = new Customer { Id = id2, PartitionKey = new string('€', 341) };

context.Customers.Add(customer1);
context.Customers.Add(customer2);

context.Customers.Add(customer);
await context.SaveChangesAsync();
ListLoggerFactory.Clear();

// Total document size will be: 2_097_503. Total request size will be: 2_098_541
// Normally 2MiB is 2_097_152, but cosmos appears to allow ~1Kib (1389 bytes) extra
var str = new string('x', 2_095_228);
customer.Name = str;
customer1.Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate);
customer2.Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate);

if (oneByteOver)
await context.SaveChangesAsync();
using var assertContext = contextFactory.CreateContext();
Assert.Equal(2, (await context.Customers.ToListAsync()).Count);

// The id being a special character should make the difference whether this fits in 1 batch.
if (isIdSpecialChar)
{
customer.Name += 'x';
var ex = await Assert.ThrowsAsync<DbUpdateException>(() => context.SaveChangesAsync());
Assert.IsType<CosmosException>(ex.InnerException);
Assert.Equal(2, ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}
else
{
await context.SaveChangesAsync();

using var assertContext = contextFactory.CreateContext();
var dbCustomer = await assertContext.Customers.FirstAsync();
Assert.Equal(dbCustomer.Name, str);
Assert.Equal(1, ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}
}

[ConditionalTheory, InlineData(true), InlineData(false)]
public virtual async Task SaveChanges_update_id_contains_special_chars_which_makes_request_larger_than_2_mib_splits_into_2_batches(bool isIdSpecialChar)
public virtual async Task SaveChanges_create_id_contains_special_chars_which_would_make_request_larger_than_2_mib_on_update_does_not_split_into_2_batches_for_create(bool isIdSpecialChar)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();

using var context = contextFactory.CreateContext();

string id1 = isIdSpecialChar ? new string('€', 341) : new string('x', 341);
string id2 = isIdSpecialChar ? new string('Ω', 341) : new string('y', 341);
var id1 = isIdSpecialChar ? new string('€', 341) : new string('x', 341);
var id2 = isIdSpecialChar ? new string('Ω', 341) : new string('y', 341);

var customer1 = new Customer { Id = id1, PartitionKey = new string('€', 341) };
var customer2 = new Customer { Id = id2, PartitionKey = new string('€', 341) };
var customer1 = new Customer { Id = id1, Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate), PartitionKey = new string('€', 341) };
var customer2 = new Customer { Id = id2, Name = new string('x', nameLengthToExceed2MiBWithSpecialCharIdOnUpdate), PartitionKey = new string('€', 341) };

context.Customers.Add(customer1);
context.Customers.Add(customer2);

await context.SaveChangesAsync();
ListLoggerFactory.Clear();
using var assertContext = contextFactory.CreateContext();
Assert.Equal(2, (await context.Customers.ToListAsync()).Count);

// The id being a special character should not make the difference whether this fits in 1 batch, as id is duplicated in the payload on create.
Assert.Equal(1, ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
}

[ConditionalTheory, InlineData(true), InlineData(false)]
[CosmosCondition(CosmosCondition.IsNotEmulator)]
public virtual async Task SaveChanges_transaction_behaviour_always_single_entity_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over(bool oneByteOver)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();

using var context = contextFactory.CreateContext();
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;

customer1.Name = new string('x', 1046358);
customer2.Name = new string('x', 1046358);
var customer = new Customer { Id = new string('x', 1_000), PartitionKey = new string('x', 1_000) };

context.Customers.Add(customer);
await context.SaveChangesAsync();
using var assertContext = contextFactory.CreateContext();
Assert.Equal(2, (await context.Customers.ToListAsync()).Count);

// The id being a special character should make the difference whether this fits in 1 batch.
if (isIdSpecialChar)
// Total document size will be: 2_097_510. Total request size will be: 2_098_548
// Normally 2MiB is 2_097_152, but cosmos appears to allow ~1Kib (1396 bytes) extra
var str = new string('x', 2_095_235);
customer.Name = str;

if (oneByteOver)
{
Assert.Equal(2, ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
customer.Name += 'x';
var ex = await Assert.ThrowsAsync<DbUpdateException>(() => context.SaveChangesAsync());
Assert.IsType<CosmosException>(ex.InnerException);
}
else
{
Assert.Equal(1, ListLoggerFactory.Log.Count(x => x.Id == CosmosEventId.ExecutedTransactionalBatch));
await context.SaveChangesAsync();

using var assertContext = contextFactory.CreateContext();
var dbCustomer = await assertContext.Customers.FirstAsync();
Assert.Equal(dbCustomer.Name, str);
}
}

[ConditionalTheory, InlineData(true), InlineData(false)]
[CosmosCondition(CosmosCondition.IsNotEmulator)]
public virtual async Task SaveChanges_transaction_behaviour_always_update_entities_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over(bool oneByteOver)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();
Expand All @@ -470,8 +514,8 @@ public virtual async Task SaveChanges_transaction_behaviour_always_update_entiti

await context.SaveChangesAsync();

customer1.Name = new string('x', 1097582);
customer2.Name = new string('x', 1097583);
customer1.Name = new string('x', 1097589);
customer2.Name = new string('x', 1097590);

if (oneByteOver)
{
Expand All @@ -486,6 +530,7 @@ public virtual async Task SaveChanges_transaction_behaviour_always_update_entiti
}

[ConditionalTheory, InlineData(true), InlineData(false)]
[CosmosCondition(CosmosCondition.IsNotEmulator)]
public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_update(bool oneByteOver)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();
Expand All @@ -495,14 +540,14 @@ public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_up

var customer1 = new Customer { Id = new string('x', 1), PartitionKey = new string('x', 1_023) };
var customer2 = new Customer { Id = new string('y', 1_023), PartitionKey = new string('x', 1_023) };

context.Customers.Add(customer1);
context.Customers.Add(customer2);

await context.SaveChangesAsync();

customer1.Name = new string('x', 1097582 + 1_022 * 2 + 1);
customer2.Name = new string('x', 1097583);
customer1.Name = new string('x', 1097590 + 1_022 * 2);
customer2.Name = new string('x', 1097590);

if (oneByteOver)
{
Expand All @@ -517,15 +562,16 @@ public virtual async Task SaveChanges_id_counts_double_toward_request_size_on_up
}

[ConditionalTheory, InlineData(true), InlineData(false)]
[CosmosCondition(CosmosCondition.IsNotEmulator)]
public virtual async Task SaveChanges_transaction_behaviour_always_create_entities_payload_can_be_exactly_cosmos_limit_and_throws_when_1byte_over(bool oneByteOver)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();

using var context = contextFactory.CreateContext();
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;

var customer1 = new Customer { Id = new string('x', 1_023), Name = new string('x', 1098841), PartitionKey = new string('x', 1_023) };
var customer2 = new Customer { Id = new string('y', 1_023), Name = new string('x', 1098841), PartitionKey = new string('x', 1_023) };
var customer1 = new Customer { Id = new string('x', 1_023), Name = new string('x', 1098848), PartitionKey = new string('x', 1_023) };
var customer2 = new Customer { Id = new string('y', 1_023), Name = new string('x', 1098848), PartitionKey = new string('x', 1_023) };
if (oneByteOver)
{
customer1.Name += 'x';
Expand All @@ -545,15 +591,16 @@ public virtual async Task SaveChanges_transaction_behaviour_always_create_entiti
}

[ConditionalTheory, InlineData(true), InlineData(false)]
[CosmosCondition(CosmosCondition.IsNotEmulator)]
public virtual async Task SaveChanges_id_does_not_count_double_toward_request_size_on_create(bool oneByteOver)
{
var contextFactory = await InitializeAsync<TransactionalBatchContext>();

using var context = contextFactory.CreateContext();
context.Database.AutoTransactionBehavior = AutoTransactionBehavior.Always;

var customer1 = new Customer { Id = new string('x', 1), Name = new string('x', 1098841 + 1_022), PartitionKey = new string('x', 1_023) };
var customer2 = new Customer { Id = new string('y', 1_023), Name = new string('x', 1098841), PartitionKey = new string('x', 1_023) };
var customer1 = new Customer { Id = new string('x', 1), Name = new string('x', 1098848 + 1_022), PartitionKey = new string('x', 1_023) };
var customer2 = new Customer { Id = new string('y', 1_023), Name = new string('x', 1098848), PartitionKey = new string('x', 1_023) };
if (oneByteOver)
{
customer1.Name += 'x';
Expand Down