Skip to content
Merged
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
57 changes: 45 additions & 12 deletions src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3542,25 +3542,58 @@ static List<ColumnExpression> ExtractColumnsFromProjectionMapping(

static SqlExpression? RemoveRedundantNullChecks(SqlExpression predicate, List<SqlExpression> outerColumnExpressions)
{
if (predicate is SqlBinaryExpression sqlBinaryExpression)
// A single key comparison join predicate is processed with SQL null semantics in SqlNullabilityProcessor, so the
// outer key null check is genuinely redundant and is removed. But when the predicate is compound (e.g. the collection
// navigation has an additional filter), it's expanded with C# null semantics, which would incorrectly match rows where
// both keys are null. In that case the null check on a nullable outer key (e.g. the principal entity is reached through
// an optional navigation) must be kept to suppress that compensation. See #35706.
var keepNullableKeyChecks = CountNonNullCheckConjuncts(predicate, outerColumnExpressions) > 1;

return Rewrite(predicate, outerColumnExpressions, keepNullableKeyChecks);

static SqlExpression? Rewrite(
SqlExpression predicate, List<SqlExpression> outerColumnExpressions, bool keepNullableKeyChecks)
{
if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual
&& outerColumnExpressions.Contains(sqlBinaryExpression.Left)
&& sqlBinaryExpression.Right is SqlConstantExpression { Value: null })
if (predicate is SqlBinaryExpression sqlBinaryExpression)
{
return null;
}
if (sqlBinaryExpression.OperatorType == ExpressionType.NotEqual
&& outerColumnExpressions.Contains(sqlBinaryExpression.Left)
&& sqlBinaryExpression.Right is SqlConstantExpression { Value: null }
&& !(keepNullableKeyChecks && sqlBinaryExpression.Left is ColumnExpression { IsNullable: true }))
{
return null;
}

if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso)
{
var leftPredicate = RemoveRedundantNullChecks(sqlBinaryExpression.Left, outerColumnExpressions);
var rightPredicate = RemoveRedundantNullChecks(sqlBinaryExpression.Right, outerColumnExpressions);
if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso)
{
var leftPredicate = Rewrite(sqlBinaryExpression.Left, outerColumnExpressions, keepNullableKeyChecks);
var rightPredicate = Rewrite(sqlBinaryExpression.Right, outerColumnExpressions, keepNullableKeyChecks);

return CombineNonNullExpressions(leftPredicate, rightPredicate);
return CombineNonNullExpressions(leftPredicate, rightPredicate);
}
}

return predicate;
}

return predicate;
// Counts the non-null-check conjuncts in the extracted join predicate; when more than one remains the resulting join
// predicate is a conjunction subject to C# null semantics expansion.
static int CountNonNullCheckConjuncts(SqlExpression predicate, List<SqlExpression> outerColumnExpressions)
{
Comment thread
AndriySvyryd marked this conversation as resolved.
if (predicate is SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } andAlso)
{
return CountNonNullCheckConjuncts(andAlso.Left, outerColumnExpressions)
+ CountNonNullCheckConjuncts(andAlso.Right, outerColumnExpressions);
}

return predicate is SqlBinaryExpression
{
OperatorType: ExpressionType.NotEqual, Right: SqlConstantExpression { Value: null }
} nullCheck
&& outerColumnExpressions.Contains(nullCheck.Left)
? 0
: 1;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1650,4 +1650,83 @@ public class Book
}

#endregion

#region 35706

[Theory, MemberData(nameof(IsAsyncData))]
public virtual async Task Filtered_collection_through_optional_navigation_does_not_match_on_null_keys(bool async)
{
var contextFactory = await InitializeNonSharedTest<Context35706>(seed: c => c.SeedAsync());
using var context = contextFactory.CreateDbContext();

var query = context.People
.OrderBy(p => p.PersonId)
.Select(
subject => new
{
subject.Name,
Coworkers = subject.Employer.Employees
.Where(employee => employee != subject)
.Select(coworker => new { coworker.Name })
.ToList()
});

var people = async
? await query.ToListAsync()
: query.ToList();

var satya = people.Single(p => p.Name == "Satya");
Assert.Equal(2, satya.Coworkers.Count);
Assert.Contains(satya.Coworkers, c => c.Name == "Brad");
Assert.Contains(satya.Coworkers, c => c.Name == "Amy");

var donald = people.Single(p => p.Name == "Donald");
Assert.Empty(donald.Coworkers);
}

protected class Context35706(DbContextOptions options) : DbContext(options)
{
public DbSet<Employer35706> Employers { get; set; }
public DbSet<Person35706> People { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Employer35706>().HasKey(e => e.EmployerId);
modelBuilder.Entity<Person35706>().HasKey(p => p.PersonId);
modelBuilder.Entity<Employer35706>()
.HasMany(e => e.Employees)
.WithOne(p => p.Employer)
.HasForeignKey(p => p.EmployerId);
}

public Task SeedAsync()
{
var microsoft = new Employer35706 { Name = "Microsoft" };
AddRange(
new Person35706 { Name = "Satya", Employer = microsoft },
new Person35706 { Name = "Brad", Employer = microsoft },
new Person35706 { Name = "Amy", Employer = microsoft },
new Person35706 { Name = "Donald" },
new Person35706 { Name = "Elon" });

return SaveChangesAsync();
}

public class Employer35706
{
public int EmployerId { get; set; }
public string Name { get; set; }
public IList<Person35706> Employees { get; set; }
}

public class Person35706
{
public int PersonId { get; set; }
public string Name { get; set; }
public int? EmployerId { get; set; }
public Employer35706 Employer { get; set; }
}
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,20 @@ SELECT COUNT(*)
FROM [Books] AS [b]
WHERE [a].[AuthorId] = [b].[AuthorId]) AS [BooksCount]
FROM [Authors] AS [a]
""");
}

public override async Task Filtered_collection_through_optional_navigation_does_not_match_on_null_keys(bool async)
{
await base.Filtered_collection_through_optional_navigation_does_not_match_on_null_keys(async);

AssertSql(
"""
SELECT [p].[Name], [p].[PersonId], [p0].[Name], [p0].[PersonId]
FROM [People] AS [p]
LEFT JOIN [Employers] AS [e] ON [p].[EmployerId] = [e].[EmployerId]
LEFT JOIN [People] AS [p0] ON [e].[EmployerId] IS NOT NULL AND [e].[EmployerId] = [p0].[EmployerId] AND [p].[PersonId] <> [p0].[PersonId]
ORDER BY [p].[PersonId]
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3148,7 +3148,7 @@ FROM [LocustLeaders] AS [l]
WHERE [l].[Discriminator] = N'LocustCommander'
) AS [l0] ON [f].[CommanderName] = [l0].[Name]
LEFT JOIN [Gears] AS [g] ON [l0].[DefeatedByNickname] = [g].[Nickname] AND [l0].[DefeatedBySquadId] = [g].[SquadId]
LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [g0].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [g0].[LeaderSquadId]
LEFT JOIN [Gears] AS [g0] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [g0].[LeaderNickname] AND [g].[SquadId] = [g0].[LeaderSquadId]
ORDER BY [f].[Id], [g0].[Nickname]
""");
}
Expand All @@ -3167,7 +3167,7 @@ FROM [LocustLeaders] AS [l]
WHERE [l].[Discriminator] = N'LocustCommander'
) AS [l0] ON [f].[CommanderName] = [l0].[Name]
LEFT JOIN [Gears] AS [g] ON [l0].[DefeatedByNickname] = [g].[Nickname] AND [l0].[DefeatedBySquadId] = [g].[SquadId]
LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [g0].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [g0].[LeaderSquadId]
LEFT JOIN [Gears] AS [g0] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [g0].[LeaderNickname] AND [g].[SquadId] = [g0].[LeaderSquadId]
ORDER BY [f].[Id], [g0].[Nickname]
""");
}
Expand Down Expand Up @@ -3210,7 +3210,7 @@ LEFT JOIN (
SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], [c].[Name], [c].[Location], [c].[Nation]
FROM [Gears] AS [g0]
INNER JOIN [Cities] AS [c] ON [g0].[CityOfBirthName] = [c].[Name]
) AS [s] ON ([g].[Nickname] = [s].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [s].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [s].[LeaderSquadId]
) AS [s] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [s].[LeaderNickname] AND [g].[SquadId] = [s].[LeaderSquadId]
ORDER BY [l].[Name], [s].[Nickname]
""");
}
Expand Down Expand Up @@ -3332,7 +3332,7 @@ FROM [LocustLeaders] AS [l]
WHERE [l].[Discriminator] = N'LocustCommander'
) AS [l0] ON [f].[CommanderName] = [l0].[Name]
LEFT JOIN [Gears] AS [g] ON [l0].[DefeatedByNickname] = [g].[Nickname] AND [l0].[DefeatedBySquadId] = [g].[SquadId]
LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [g0].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [g0].[LeaderSquadId]
LEFT JOIN [Gears] AS [g0] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [g0].[LeaderNickname] AND [g].[SquadId] = [g0].[LeaderSquadId]
ORDER BY [f].[Id], [g0].[Nickname]
""");
}
Expand Down Expand Up @@ -3385,7 +3385,7 @@ FROM [LocustLeaders] AS [l]
WHERE [l].[Discriminator] = N'LocustCommander'
) AS [l0] ON [f].[CommanderName] = [l0].[Name]
LEFT JOIN [Gears] AS [g] ON [l0].[DefeatedByNickname] = [g].[Nickname] AND [l0].[DefeatedBySquadId] = [g].[SquadId]
LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [g0].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [g0].[LeaderSquadId]
LEFT JOIN [Gears] AS [g0] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [g0].[LeaderNickname] AND [g].[SquadId] = [g0].[LeaderSquadId]
ORDER BY [f].[Id], [g0].[Nickname]
""");
}
Expand Down Expand Up @@ -4292,7 +4292,7 @@ LEFT JOIN (
FROM [Gears] AS [g]
WHERE [g].[Discriminator] = N'Officer'
) AS [g0] ON [t].[GearNickName] = [g0].[Nickname]
LEFT JOIN [Gears] AS [g1] ON ([g0].[Nickname] = [g1].[LeaderNickname] OR ([g0].[Nickname] IS NULL AND [g1].[LeaderNickname] IS NULL)) AND [g0].[SquadId] = [g1].[LeaderSquadId]
LEFT JOIN [Gears] AS [g1] ON [g0].[Nickname] IS NOT NULL AND [g0].[SquadId] IS NOT NULL AND [g0].[Nickname] = [g1].[LeaderNickname] AND [g0].[SquadId] = [g1].[LeaderSquadId]
ORDER BY [t].[Id], [g0].[Nickname], [g0].[SquadId], [g1].[Nickname]
""");
}
Expand Down Expand Up @@ -6185,7 +6185,7 @@ LEFT JOIN (
FROM [Gears] AS [g0]
) AS [g1]
WHERE [g1].[row] <= 50
) AS [g2] ON ([g].[Nickname] = [g2].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [g2].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [g2].[LeaderSquadId]
) AS [g2] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [g2].[LeaderNickname] AND [g].[SquadId] = [g2].[LeaderSquadId]
WHERE [g].[Discriminator] = N'Officer'
ORDER BY [t].[Id], [g2].[Nickname]
""");
Expand All @@ -6200,7 +6200,7 @@ public override async Task Project_collection_navigation_nested_composite_key(bo
SELECT [t].[Id], [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[Discriminator], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank]
FROM [Tags] AS [t]
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId]
LEFT JOIN [Gears] AS [g0] ON ([g].[Nickname] = [g0].[LeaderNickname] OR ([g].[Nickname] IS NULL AND [g0].[LeaderNickname] IS NULL)) AND [g].[SquadId] = [g0].[LeaderSquadId]
LEFT JOIN [Gears] AS [g0] ON [g].[Nickname] IS NOT NULL AND [g].[SquadId] IS NOT NULL AND [g].[Nickname] = [g0].[LeaderNickname] AND [g].[SquadId] = [g0].[LeaderSquadId]
WHERE [g].[Discriminator] = N'Officer'
ORDER BY [t].[Id], [g0].[Nickname]
""");
Expand All @@ -6214,7 +6214,7 @@ public override async Task Null_checks_in_correlated_predicate_are_correctly_tra
"""
SELECT [t].[Id], [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank]
FROM [Tags] AS [t]
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] = [g].[SquadId] AND [t].[Note] IS NOT NULL
LEFT JOIN [Gears] AS [g] ON [t].[GearNickName] = [g].[Nickname] AND [t].[GearSquadId] IS NOT NULL AND [t].[GearSquadId] = [g].[SquadId] AND [t].[GearNickName] IS NOT NULL AND [t].[Note] IS NOT NULL
ORDER BY [t].[Id], [g].[Nickname]
""");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4187,7 +4187,7 @@ FROM [Gears] AS [g0]
UNION ALL
SELECT [o0].[Nickname], [o0].[SquadId], [o0].[AssignedCityName], [o0].[CityOfBirthName], [o0].[FullName], [o0].[HasSoulPatch], [o0].[LeaderNickname], [o0].[LeaderSquadId], [o0].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o0]
) AS [u0] ON ([u].[Nickname] = [u0].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u0].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u0].[LeaderSquadId]
) AS [u0] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u0].[LeaderNickname] AND [u].[SquadId] = [u0].[LeaderSquadId]
ORDER BY [l].[Id], [u0].[Nickname]
""");
}
Expand All @@ -4214,7 +4214,7 @@ FROM [Gears] AS [g0]
UNION ALL
SELECT [o0].[Nickname], [o0].[SquadId], [o0].[AssignedCityName], [o0].[CityOfBirthName], [o0].[FullName], [o0].[HasSoulPatch], [o0].[LeaderNickname], [o0].[LeaderSquadId], [o0].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o0]
) AS [u0] ON ([u].[Nickname] = [u0].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u0].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u0].[LeaderSquadId]
) AS [u0] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u0].[LeaderNickname] AND [u].[SquadId] = [u0].[LeaderSquadId]
ORDER BY [l].[Id], [u0].[Nickname]
""");
}
Expand Down Expand Up @@ -4299,7 +4299,7 @@ UNION ALL
FROM [Officers] AS [o0]
) AS [u1]
INNER JOIN [Cities] AS [c] ON [u1].[CityOfBirthName] = [c].[Name]
) AS [s] ON ([u0].[Nickname] = [s].[LeaderNickname] OR ([u0].[Nickname] IS NULL AND [s].[LeaderNickname] IS NULL)) AND [u0].[SquadId] = [s].[LeaderSquadId]
) AS [s] ON [u0].[Nickname] IS NOT NULL AND [u0].[SquadId] IS NOT NULL AND [u0].[Nickname] = [s].[LeaderNickname] AND [u0].[SquadId] = [s].[LeaderSquadId]
ORDER BY [u].[Name], [s].[Nickname]
""");
}
Expand Down Expand Up @@ -4513,7 +4513,7 @@ FROM [Gears] AS [g0]
UNION ALL
SELECT [o0].[Nickname], [o0].[SquadId], [o0].[AssignedCityName], [o0].[CityOfBirthName], [o0].[FullName], [o0].[HasSoulPatch], [o0].[LeaderNickname], [o0].[LeaderSquadId], [o0].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o0]
) AS [u0] ON ([u].[Nickname] = [u0].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u0].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u0].[LeaderSquadId]
) AS [u0] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u0].[LeaderNickname] AND [u].[SquadId] = [u0].[LeaderSquadId]
ORDER BY [l].[Id], [u0].[Nickname]
""");
}
Expand Down Expand Up @@ -4604,7 +4604,7 @@ FROM [Gears] AS [g0]
UNION ALL
SELECT [o0].[Nickname], [o0].[SquadId], [o0].[AssignedCityName], [o0].[CityOfBirthName], [o0].[FullName], [o0].[HasSoulPatch], [o0].[LeaderNickname], [o0].[LeaderSquadId], [o0].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o0]
) AS [u0] ON ([u].[Nickname] = [u0].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u0].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u0].[LeaderSquadId]
) AS [u0] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u0].[LeaderNickname] AND [u].[SquadId] = [u0].[LeaderSquadId]
ORDER BY [l].[Id], [u0].[Nickname]
""");
}
Expand Down Expand Up @@ -5787,7 +5787,7 @@ FROM [Gears] AS [g]
UNION ALL
SELECT [o0].[Nickname], [o0].[SquadId], [o0].[FullName], [o0].[LeaderNickname], [o0].[LeaderSquadId]
FROM [Officers] AS [o0]
) AS [u0] ON ([u].[Nickname] = [u0].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u0].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u0].[LeaderSquadId]
) AS [u0] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u0].[LeaderNickname] AND [u].[SquadId] = [u0].[LeaderSquadId]
ORDER BY [t].[Id], [u].[Nickname], [u].[SquadId], [u0].[Nickname]
""");
}
Expand Down Expand Up @@ -8353,7 +8353,7 @@ FROM [Officers] AS [o0]
) AS [u0]
) AS [u1]
WHERE [u1].[row] <= 50
) AS [u2] ON ([u].[Nickname] = [u2].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u2].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u2].[LeaderSquadId]
) AS [u2] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u2].[LeaderNickname] AND [u].[SquadId] = [u2].[LeaderSquadId]
WHERE [u].[Discriminator] = N'Officer'
ORDER BY [t].[Id], [u2].[Nickname]
""");
Expand All @@ -8380,7 +8380,7 @@ FROM [Gears] AS [g0]
UNION ALL
SELECT [o0].[Nickname], [o0].[SquadId], [o0].[AssignedCityName], [o0].[CityOfBirthName], [o0].[FullName], [o0].[HasSoulPatch], [o0].[LeaderNickname], [o0].[LeaderSquadId], [o0].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o0]
) AS [u0] ON ([u].[Nickname] = [u0].[LeaderNickname] OR ([u].[Nickname] IS NULL AND [u0].[LeaderNickname] IS NULL)) AND [u].[SquadId] = [u0].[LeaderSquadId]
) AS [u0] ON [u].[Nickname] IS NOT NULL AND [u].[SquadId] IS NOT NULL AND [u].[Nickname] = [u0].[LeaderNickname] AND [u].[SquadId] = [u0].[LeaderSquadId]
WHERE [u].[Discriminator] = N'Officer'
ORDER BY [t].[Id], [u0].[Nickname]
""");
Expand All @@ -8400,7 +8400,7 @@ FROM [Gears] AS [g]
UNION ALL
SELECT [o].[Nickname], [o].[SquadId], [o].[AssignedCityName], [o].[CityOfBirthName], [o].[FullName], [o].[HasSoulPatch], [o].[LeaderNickname], [o].[LeaderSquadId], [o].[Rank], N'Officer' AS [Discriminator]
FROM [Officers] AS [o]
) AS [u] ON [t].[GearNickName] = [u].[Nickname] AND [t].[GearSquadId] = [u].[SquadId] AND [t].[Note] IS NOT NULL
) AS [u] ON [t].[GearNickName] = [u].[Nickname] AND [t].[GearSquadId] IS NOT NULL AND [t].[GearSquadId] = [u].[SquadId] AND [t].[GearNickName] IS NOT NULL AND [t].[Note] IS NOT NULL
ORDER BY [t].[Id], [u].[Nickname]
""");
}
Expand Down
Loading
Loading