-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Query: Select after complex GroupJoin leads to unpredictable results #4858
Comments
I paste an test code for both at #4860 (comment) |
This appears to be a bug (or multiple bugs) with the shape of the result from public class ComplexGroupJoin
{
[Fact]
public void Group_join_without_projection()
{
AssertQuery(
(dmus, udms) => dmus
.GroupJoin(
udms.Where(udp => udp.UserId == 1025 && udp.DeviceId != null),
dmu => dmu.DeviceId,
udp => udp.DeviceId,
(key, vals) => new { key, vals })
.Where(pair => pair.vals.Any()),
asserter:
(l2oResults, efResults) =>
{
var l2oObjects
= l2oResults
.OfType<object[]>()
.Select(o => o.Select(o2 =>
new
{
Key = GetProperty<DeviceMsgUp>(o2, "key"),
Vals = string.Join(", ", GetProperty<IEnumerable<UserDeviceMap>>(o2, "vals")
.Select(dum => dum.UdMapId.ToString()))
}));
var efObjects
= efResults
.OfType<object[]>()
.Select(o => o.Select(o2 =>
new
{
Key = GetProperty<DeviceMsgUp>(o2, "key"),
Vals = string.Join(", ", GetProperty<IEnumerable<UserDeviceMap>>(o2, "vals")
.Select(dum => dum.UdMapId.ToString()))
}));
Assert.Equal(l2oObjects, efObjects);
});
// Expected: WhereSelectEnumerableIterator<Object[], IEnumerable<<>f__AnonymousType10<DeviceMsgUp, String>>> [[{ Key = DeviceMsgUp 1, Vals = 1 }, { Key = DeviceMsgUp 2, Vals = 1 }]]
// Actual: WhereSelectEnumerableIterator<Object[], IEnumerable<<>f__AnonymousType10<DeviceMsgUp, String>>> [[{ Key = DeviceMsgUp 1, Vals = 1, 1 }]]
}
[Fact]
public void Group_join_with_key_projection()
{
AssertQuery(
(dmus, udms) => dmus
.GroupJoin(
udms.Where(udp => udp.UserId == 1025 && udp.DeviceId != null),
dmu => dmu.DeviceId,
udp => udp.DeviceId,
(key, vals) => new { key, vals })
.Where(pair => pair.vals.Any())
.Select(pair => pair.key),
asserter:
(l2oResults, efResults) =>
{
var l2oObjects
= l2oResults
.OfType<object[]>()
.Select(o => o.Select(o2 => (DeviceMsgUp)o2));
var efObjects
= efResults
.OfType<object[]>()
.Select(o => o.Select(o2 => (DeviceMsgUp)o2));
Assert.Equal(l2oObjects, efObjects);
});
// Expected: WhereSelectEnumerableIterator<Object[], IEnumerable<DeviceMsgUp>> [[DeviceMsgUp 1, DeviceMsgUp 2]]
// Actual: WhereSelectEnumerableIterator<Object[], IEnumerable<DeviceMsgUp>> [[DeviceMsgUp 1, DeviceMsgUp 2, DeviceMsgUp 3]]
}
[Fact]
public void Group_join_with_key_action_projection()
{
AssertQuery(
(dmus, udms) => dmus
.GroupJoin(
udms.Where(udp => udp.UserId == 1025 && udp.DeviceId != null),
dmu => dmu.DeviceId,
udp => udp.DeviceId,
(key, vals) => new { key, vals })
.Where(pair => pair.vals.Any())
.Select(pair => pair.key)
.Select(key => new { key.Action }),
asserter:
(l2oResults, efResults) =>
{
var l2oObjects
= l2oResults
.OfType<object[]>()
.Select(o => o.Select(o2 => GetProperty<byte>(o2, "Action")));
var efObjects
= efResults
.OfType<object[]>()
.Select(o => o.Select(o2 => GetProperty<byte>(o2, "Action")));
Assert.Equal(l2oObjects, efObjects);
});
// Passes (Probably by coincidence.)
}
#region test_setup
public ComplexGroupJoin()
{
using (var context = new MyDbContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.UserDeviceMaps.AddRange(_userDeviceMaps);
context.DeviceMsgUps.AddRange(_deviceMsgUps);
context.SaveChanges();
}
}
private readonly List<DeviceMsgUp> _deviceMsgUps
= new List<DeviceMsgUp>
{
new DeviceMsgUp
{
MessageId = 1,
DeviceId = 1,
Action = 27,
},
new DeviceMsgUp
{
MessageId = 2,
DeviceId = 1,
Action = 27,
},
new DeviceMsgUp
{
MessageId = 3,
DeviceId = 2,
Action = 27,
}
};
private readonly List<UserDeviceMap> _userDeviceMaps
= new List<UserDeviceMap>
{
new UserDeviceMap
{
UdMapId = 1,
UserId = 1025,
DeviceId = 1
}
};
private static T GetProperty<T>(object o, string propertyName)
=> (T)o.GetType().GetProperty(propertyName).GetValue(o);
private void AssertQuery(
Func<IQueryable<DeviceMsgUp>, IQueryable<UserDeviceMap>, IQueryable<object>> query,
bool assertOrder = false,
Action<IList<object>, IList<object>> asserter = null)
{
using (var context = new MyDbContext())
{
TestHelpers.AssertResults(
new[] { query(_deviceMsgUps.AsQueryable(), _userDeviceMaps.AsQueryable()).ToArray() },
new[] { query(context.Set<DeviceMsgUp>(), context.Set<UserDeviceMap>()).ToArray() },
assertOrder,
asserter);
}
}
#endregion
#region model
public class MyDbContext : DbContext
{
public DbSet<DeviceMsgUp> DeviceMsgUps { get; set; }
public DbSet<UserDeviceMap> UserDeviceMaps { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=ConfusingErrorTest;Integrated Security=True");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserDeviceMap>()
.HasKey(udm => udm.UdMapId);
modelBuilder.Entity<UserDeviceMap>()
.Property(udm => udm.UdMapId).ValueGeneratedNever();
modelBuilder.Entity<DeviceMsgUp>()
.HasKey(dmu => dmu.MessageId);
modelBuilder.Entity<DeviceMsgUp>()
.Property(dmu => dmu.MessageId).ValueGeneratedNever();
}
}
public class DeviceMsgUp
{
public int MessageId { get; set; } //(Primary key)
public int DeviceId { get; set; }
public byte Action { get; set; }
protected bool Equals(DeviceMsgUp other) => string.Equals(MessageId, other.MessageId);
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
return obj.GetType() == GetType()
&& Equals((DeviceMsgUp)obj);
}
public override int GetHashCode() => MessageId.GetHashCode();
public override string ToString() => "DeviceMsgUp " + MessageId;
}
public class UserDeviceMap
{
public int UdMapId { get; set; } //(Primary key)
public int UserId { get; set; }
public int? DeviceId { get; set; }
protected bool Equals(UserDeviceMap other) => string.Equals(UdMapId, other.UdMapId);
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
return obj.GetType() == GetType()
&& Equals((UserDeviceMap)obj);
}
public override int GetHashCode() => UdMapId.GetHashCode();
public override string ToString() => "Customer " + UdMapId;
}
#endregion
} |
Clearing milestone for triage |
Currently 1st and 3rd queries pass and only 2nd query fails. We produce the following query plan:
The problem is that GroupJoin's inner elements is IEnumerable, which gets created even for rows that don't have a Device (as opposed to case where we materialize Inner as Entity - then we correctly make it null, and those rows are filtered correctly) |
Clearing milestone for triage, we might want to take this for 1.0.1 since it can cause data corruption @divega |
…esult Problem exists for complex query with groupjoin, where the inner element is a subquery, then we apply collection operator on the inner collection, but project only the outer element like so: context.LevelOne .GroupJoin( context.LevelTwo.Where(l2 => l2.Name != "L2 01"), l1 => l1.Id, l2 => l2.Level1_Optional_Id, (l1, l2s) => new { l1, l2s }) .Where(r => r.l2s.Any()) Select(r => r.l1); What happens is that we look at which query sources we need to materialize (based on final projection), deduce that we don't need to materialize the elements of the inner collection, so it's being represented as value buffer. Problem is that currently value buffer can't represent a null-value element, so Any() always returns true (even for empty collection), which leads to incorrect results. Fix is to force materialization of inner collection elements as entities. We already did that, but missed the case where the inner collection was a subquery.
Approved for 1.0.1. |
…esult Problem exists for complex query with groupjoin, where the inner element is a subquery, then we apply collection operator on the inner collection, but project only the outer element like so: context.LevelOne .GroupJoin( context.LevelTwo.Where(l2 => l2.Name != "L2 01"), l1 => l1.Id, l2 => l2.Level1_Optional_Id, (l1, l2s) => new { l1, l2s }) .Where(r => r.l2s.Any()) Select(r => r.l1); What happens is that we look at which query sources we need to materialize (based on final projection), deduce that we don't need to materialize the elements of the inner collection, so it's being represented as value buffer. Problem is that currently value buffer can't represent a null-value element, so Any() always returns true (even for empty collection), which leads to incorrect results. Fix is to force materialization of inner collection elements as entities. We already did that, but missed the case where the inner collection was a subquery.
Database:
MSSQL
EntityFramework:
EntityFramework.Core:7.0.0-rc1-final
andMicrosoft.EntityFrameworkCore:1.0.0-rc2-20261(aspnetcidev)
Get 3 result(the same with ToList).
Get 57 result(the same with ToList).
Get 1 result(the same with ToList).
Model
The text was updated successfully, but these errors were encountered: