Skip to content

Improved select parsing #3782

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

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
76 changes: 76 additions & 0 deletions src/LinqTests/Acceptance/select_clause_usage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JasperFx.Core;
using LinqTests.Acceptance.Support;
using Marten;
using Marten.Exceptions;
using Marten.Services.Json;
using Marten.Testing.Documents;
using Marten.Testing.Harness;
Expand Down Expand Up @@ -389,6 +390,58 @@ public async Task use_select_in_query_for_one_object_property()
actual.Number.ShouldBe(target.Inner.Number);
}

[Fact]
public async Task select_to_class_with_primary_constructor_throws_exception()
{
theSession.Store(new User { FirstName = "Hank", LastName = "Aaron" });
theSession.Store(new User { FirstName = "Bill", LastName = "Laimbeer" });
await theSession.SaveChangesAsync();

var projections = theSession.Query<User>()
.OrderBy(u => u.FirstName)
.Select(u => new UserProjection(u));

Should.Throw<BadLinqExpressionException>(() => projections.ToList());
}

[Fact]
public async Task select_to_class_with_required_properties()
{
theSession.Store(new User { FirstName = "Hank" });
await theSession.SaveChangesAsync();

var result = await theSession.Query<User>()
.Where(u => u.FirstName == "Hank")
.Select(u => new UserRequired { Name = u.FirstName })
.FirstOrDefaultAsync();

result.ShouldNotBeNull();
result.Name.ShouldBe("Hank");
}

[Fact]
public async Task select_with_complex_expression_throws_exception()
{
theSession.Store(new User { FirstName = "Hank" });
await theSession.SaveChangesAsync();

var query = theSession.Query<User>()
.Select(u => ReverseString(u.FirstName));

Should.Throw<BadLinqExpressionException>(() => query.ToList());
}

[Fact]
public async Task select_with_complex_expression_in_object_throws_exception()
{
theSession.Store(new User { FirstName = "Hank" });
await theSession.SaveChangesAsync();

var query = theSession.Query<User>()
.Select(u => new { Name = ReverseString(u.FirstName) });

Should.Throw<BadLinqExpressionException>(() => query.ToList());
}

public class FlatTarget
{
Expand All @@ -408,6 +461,14 @@ public FlatTarget(Guid id, int number, int innerNumber)
public select_clause_usage(DefaultStoreFixture fixture) : base(fixture)
{
}

// Method that should not be translatable to SQL
private static string ReverseString(string input)
{
char[] charArray = input.ToCharArray();
Array.Reverse(charArray);
return new string(charArray);
}
}


Expand All @@ -416,4 +477,19 @@ public class UserName
public string Name { get; set; }
}

public class UserProjection
{
public UserProjection(User u)
{
FirstName = u.FirstName;
LastName = u.LastName;
}

public string FirstName { get; }
public string LastName { get; }
}

public class UserRequired
{
public required string Name { get; set; }
}
213 changes: 201 additions & 12 deletions src/LinqTests/Operators/string_compare_operator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Marten.Testing.Documents;
Expand All @@ -11,29 +13,216 @@ public class string_compare_operator: IntegrationContext
[Fact]
public async Task string_compare_works()
{
theSession.Store(new Target { String = "Apple" });
theSession.Store(new Target { String = "Banana" });
theSession.Store(new Target { String = "Cherry" });
theSession.Store(new Target { String = "Durian" });
// Arrange
var targets = new[]
{
new Target { String = "Apple" },
new Target { String = "Banana" },
new Target { String = "Cherry" },
new Target { String = "Durian" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

var queryable = theSession.Query<Target>().Where(x => string.Compare(x.String, "Cherry") > 0);
// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "Cherry") > 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "Cherry") > 0)
.Select(x => x.String);

queryable.ToList().Count.ShouldBe(1);
queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

[Fact]
public async Task string_compare_to_works()
{
theSession.Store(new Target { String = "Apple" });
theSession.Store(new Target { String = "Banana" });
theSession.Store(new Target { String = "Cherry" });
theSession.Store(new Target { String = "Durian" });
// Arrange
var targets = new[]
{
new Target { String = "Apple" },
new Target { String = "Banana" },
new Target { String = "Cherry" },
new Target { String = "Durian" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

// Act
var queryable = theSession.Query<Target>()
.Where(x => x.String.CompareTo("Banana") > 0);

// Assert
var expected = targets
.Where(x => x.String.CompareTo("Banana") > 0)
.Select(x => x.String);

queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

[Fact]
public async Task string_compare_ignore_case_works()
{
// Arrange
var targets = new[]
{
new Target { String = "apple" },
new Target { String = "Banana" },
new Target { String = "cherry" },
new Target { String = "Durian" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "BANANA", StringComparison.OrdinalIgnoreCase) > 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "BANANA", StringComparison.OrdinalIgnoreCase) > 0)
.Select(x => x.String);

queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

[Fact]
public async Task string_compare_with_invariant_culture_and_ignore_case_works()
{
// Arrange
var targets = new[]
{
new Target { String = "apple" },
new Target { String = "Banana" },
new Target { String = "cherry" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "APPLE", CultureInfo.InvariantCulture, CompareOptions.IgnoreCase) == 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "APPLE", CultureInfo.InvariantCulture, CompareOptions.IgnoreCase) == 0)
.Select(x => x.String);

queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

[Fact]
// Test requires the following SQL to be run: create collation "tr-TR" (locale='tr-TR.utf8');
public async Task string_compare_with_turkish_culture_case_insensitive_behavior()
{
// Arrange
var turkishCulture = CultureInfo.GetCultureInfo("tr-TR");
var targets = new[]
{
new Target { String = "İi" }, // Turkish uppercase İ
new Target { String = "ıi" } // Turkish lowercase ı
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

var queryable = theSession.Query<Target>().Where(x => x.String.CompareTo("Banana") > 0);
// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "ii", turkishCulture, CompareOptions.IgnoreCase) == 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "ii", turkishCulture, CompareOptions.IgnoreCase) == 0)
.Select(x => x.String);

queryable.Select(x => x.String).ToList().ShouldBe(expected, ignoreOrder: true);
}

[Fact]
// Test requires the following SQL to be run: create collation "en-US" (locale='en-US.utf8');
public async Task string_compare_with_english_culture_ignore_case_works()
{
// Arrange
var englishCulture = CultureInfo.GetCultureInfo("en-US");
var targets = new[]
{
new Target { String = "Apple" },
new Target { String = "apple" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "APPLE", true, englishCulture) == 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "APPLE", true, englishCulture) == 0)
.Select(x => x.String);

queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

[Fact]
// Test requires the following SQL to be run: create collation "de-DE" (locale='de-DE.utf8');
public async Task string_compare_with_german_culture_and_compare_options_works()
{
// Arrange
var germanCulture = CultureInfo.GetCultureInfo("de-DE");
var targets = new[]
{
new Target { String = "Straße" },
new Target { String = "Strasse" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "Strasse", germanCulture, CompareOptions.IgnoreSymbols) == 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "Strasse", germanCulture, CompareOptions.IgnoreSymbols) == 0)
.Select(x => x.String);

queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

[Fact]
// Test requires the following SQL to be run: create collation "da-DK" (locale='da-DK.utf8');
public async Task string_compare_to_with_culture_works()
{
// Arrange
var danishCulture = CultureInfo.GetCultureInfo("da-DK");
var targets = new[]
{
new Target { String = "Apple" },
new Target { String = "Æble" }
};

theSession.Store(targets);
await theSession.SaveChangesAsync();

// Act
var queryable = theSession.Query<Target>()
.Where(x => string.Compare(x.String, "Apple", danishCulture, CompareOptions.None) > 0);

// Assert
var expected = targets
.Where(x => string.Compare(x.String, "Apple", danishCulture, CompareOptions.None) > 0)
.Select(x => x.String);

queryable.ToList().Count.ShouldBe(2);
queryable.Select(x => x.String).ToList().ShouldBe(expected);
}

public string_compare_operator(DefaultStoreFixture fixture) : base(fixture)
Expand Down
2 changes: 1 addition & 1 deletion src/Marten/Linq/Parsing/CompareToComparable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public CompareToComparable(SimpleExpression left, SimpleExpression right)

public ISqlFragment CreateComparison(string op, ConstantExpression constant)
{
// Only compare to 0 is valid: CompareTo() > 0 → ">", CompareTo() == 0 → "=", etc.
// Only compare to 0 is valid: CompareTo() > 0 → ">", CompareTo() == 0 → "=", CompareTo() < 0 → "<"
if (constant.Value is int intValue && intValue == 0)
{
var leftFragment = _left.FindValueFragment();
Expand Down
Loading
Loading