Skip to content

TEST_ DO NOT MERGE - Add Cosmos DB query translation and collection navigation tests #35961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
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
189 changes: 189 additions & 0 deletions test/EFCore.Cosmos.Tests/Query/CosmosAdvancedTranslationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Azure.Cosmos;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace Microsoft.EntityFrameworkCore.Cosmos.Query;

public class CosmosAdvancedTranslationTest
{
[ConditionalTheory]
[InlineData(true)]
[InlineData(false)]
public async Task Math_functions_with_client_evaluation(bool async)
{
await using var testDatabase = CosmosTestStore.Create("MathTest");
var options = new DbContextOptionsBuilder()
.UseCosmos(
testDatabase.ConnectionUri,
testDatabase.AuthToken,
testDatabase.Name)
.Options;

var context = new TestContext(options);
await context.Database.EnsureCreatedAsync();

try
{
await context.AddRangeAsync(
new NumericEntity { Id = "1", Value = 1.5 },
new NumericEntity { Id = "2", Value = 2.7 },
new NumericEntity { Id = "3", Value = 3.2 });
await context.SaveChangesAsync();

// Test Log2 function which requires client evaluation
var query = context.Numbers
.Where(e => Math.Log2(e.Value) > 1.0);

var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await (async ? query.ToListAsync() : Task.FromResult(query.ToList())));

Assert.Contains("The LINQ expression", exception.Message);

// Test complex math expressions
var complexQuery = context.Numbers
.Where(e => Math.Round(Math.Pow(e.Value, 2), 2) > 5.0);

exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await (async ? complexQuery.ToListAsync() : Task.FromResult(complexQuery.ToList())));

Assert.Contains("The LINQ expression", exception.Message);
}
finally
{
await testDatabase.DisposeAsync();
}
}

[ConditionalTheory]
[InlineData(true)]
[InlineData(false)]
public async Task String_operations_with_client_evaluation(bool async)
{
await using var testDatabase = CosmosTestStore.Create("StringTest");
var options = new DbContextOptionsBuilder()
.UseCosmos(
testDatabase.ConnectionUri,
testDatabase.AuthToken,
testDatabase.Name)
.Options;

var context = new TestContext(options);
await context.Database.EnsureCreatedAsync();

try
{
await context.AddRangeAsync(
new StringEntity { Id = "1", Text = "Hello" },
new StringEntity { Id = "2", Text = "World" });
await context.SaveChangesAsync();

// Test string.IsNullOrEmpty
var query = context.Strings
.Where(e => !string.IsNullOrEmpty(e.Text));

var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await (async ? query.ToListAsync() : Task.FromResult(query.ToList())));

Assert.Contains("The LINQ expression", exception.Message);

// Test complex string comparisons
var complexQuery = context.Strings
.Where(e => e.Text.CompareTo("Test") > 0);

exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await (async ? complexQuery.ToListAsync() : Task.FromResult(complexQuery.ToList())));

Assert.Contains("The LINQ expression", exception.Message);
}
finally
{
await testDatabase.DisposeAsync();
}
}

[ConditionalTheory]
[InlineData(true)]
[InlineData(false)]
public async Task Regex_operations_with_different_options(bool async)
{
await using var testDatabase = CosmosTestStore.Create("RegexTest");
var options = new DbContextOptionsBuilder()
.UseCosmos(
testDatabase.ConnectionUri,
testDatabase.AuthToken,
testDatabase.Name)
.Options;

var context = new TestContext(options);
await context.Database.EnsureCreatedAsync();

try
{
await context.AddRangeAsync(
new StringEntity { Id = "1", Text = "Test123" },
new StringEntity { Id = "2", Text = "123Test" });
await context.SaveChangesAsync();

// Test regex with RightToLeft option (not supported)
var query = context.Strings
.Where(e => System.Text.RegularExpressions.Regex.IsMatch(
e.Text, "^Test", System.Text.RegularExpressions.RegexOptions.RightToLeft));

await Assert.ThrowsAsync<CosmosException>(
async () => await (async ? query.ToListAsync() : Task.FromResult(query.ToList())));

// Test regex with combined options
var complexQuery = context.Strings
.Where(e => System.Text.RegularExpressions.Regex.IsMatch(
e.Text,
"^test",
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Multiline));

// This should work as these options are supported
var results = async
? await complexQuery.ToListAsync()
: complexQuery.ToList();

Assert.Single(results);
}
finally
{
await testDatabase.DisposeAsync();
}
}

private class TestContext : DbContext
{
public TestContext(DbContextOptions options) : base(options)
{
}

public DbSet<NumericEntity> Numbers { get; set; }
public DbSet<StringEntity> Strings { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<NumericEntity>()
.HasPartitionKey(e => e.Id);

modelBuilder.Entity<StringEntity>()
.HasPartitionKey(e => e.Id);
}
}

private class NumericEntity
{
public string Id { get; set; }
public double Value { get; set; }
}

private class StringEntity
{
public string Id { get; set; }
public string Text { get; set; }
}
}
163 changes: 163 additions & 0 deletions test/EFCore.Cosmos.Tests/Query/CosmosCollectionNavigationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.EntityFrameworkCore.TestUtilities;
using Microsoft.Azure.Cosmos;

namespace Microsoft.EntityFrameworkCore.Cosmos.Query;

public class CosmosCollectionNavigationTest
{
[ConditionalTheory]
[InlineData(true)]
[InlineData(false)]
public async Task Collection_navigation_with_skip_take(bool async)
{
await using var testDatabase = CosmosTestStore.Create("CollectionTest");
var options = new DbContextOptionsBuilder()
.UseCosmos(
testDatabase.ConnectionUri,
testDatabase.AuthToken,
testDatabase.Name)
.Options;

var context = new TestContext(options);
await context.Database.EnsureCreatedAsync();

try
{
var order = new Order
{
Id = "1",
Items = new List<OrderItem>
{
new() { Id = "i1", Quantity = 5 },
new() { Id = "i2", Quantity = 10 },
new() { Id = "i3", Quantity = 15 }
}
};

await context.Orders.AddAsync(order);
await context.SaveChangesAsync();

// Test Skip/Take in subquery
var query = context.Orders
.Select(o => new
{
OrderId = o.Id,
TopItems = o.Items.Skip(1).Take(1)
});

// Should throw as Skip/Take not supported in subqueries
var exception = await Assert.ThrowsAsync<CosmosException>(
async () => await (async ? query.ToListAsync() : Task.FromResult(query.ToList())));

Assert.Contains("Limit/Offset not supported in subqueries", exception.Message);
}
finally
{
await testDatabase.DisposeAsync();
}
}

[ConditionalTheory]
[InlineData(true)]
[InlineData(false)]
public async Task Collection_aggregation_in_projection(bool async)
{
await using var testDatabase = CosmosTestStore.Create("AggregationTest");
var options = new DbContextOptionsBuilder()
.UseCosmos(
testDatabase.ConnectionUri,
testDatabase.AuthToken,
testDatabase.Name)
.Options;

var context = new TestContext(options);
await context.Database.EnsureCreatedAsync();

try
{
var orders = new[]
{
new Order
{
Id = "1",
Items = new List<OrderItem>
{
new() { Id = "i1", Quantity = 5 },
new() { Id = "i2", Quantity = 10 }
}
},
new Order
{
Id = "2",
Items = new List<OrderItem>
{
new() { Id = "i3", Quantity = 15 }
}
}
};

await context.Orders.AddRangeAsync(orders);
await context.SaveChangesAsync();

// Test aggregation in projection
var query = context.Orders
.Select(o => new
{
OrderId = o.Id,
TotalItems = o.Items.Count,
AverageQuantity = o.Items.Average(i => i.Quantity)
});

// Should throw as complex aggregations require client evaluation
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await (async ? query.ToListAsync() : Task.FromResult(query.ToList())));

Assert.Contains("The LINQ expression", exception.Message);

// Test nested aggregation in where clause
var complexQuery = context.Orders
.Where(o => o.Items.Sum(i => i.Quantity) >
context.Orders.Average(x => x.Items.Sum(i => i.Quantity)));

exception = await Assert.ThrowsAsync<InvalidOperationException>(
async () => await (async ? complexQuery.ToListAsync() : Task.FromResult(complexQuery.ToList())));

Assert.Contains("The LINQ expression", exception.Message);
}
finally
{
await testDatabase.DisposeAsync();
}
}

private class TestContext : DbContext
{
public TestContext(DbContextOptions options) : base(options)
{
}

public DbSet<Order> Orders { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasPartitionKey(e => e.Id)
.OwnsMany(e => e.Items);
}
}

private class Order
{
public string Id { get; set; }
public List<OrderItem> Items { get; set; }
}

private class OrderItem
{
public string Id { get; set; }
public int Quantity { get; set; }
}
}
Loading
Loading