diff --git a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs index a4123902336..4c5c4d475a0 100644 --- a/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs +++ b/src/EFCore.Relational/Query/RelationalEntityShaperExpression.cs @@ -94,7 +94,7 @@ protected override LambdaExpression GenerateMaterializationCondition(IEntityType return baseCondition; } - var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table + var table = entityType.GetViewOrTableMappings().SingleOrDefault(e => e.IsSplitEntityTypePrincipal ?? true)?.Table ?? entityType.GetDefaultMappings().Single().Table; if (table.IsOptional(entityType)) { diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 7718bb9de6e..2423778e7e4 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1320,7 +1320,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) return propertyAccess; } - var table = entityType.GetViewOrTableMappings().SingleOrDefault()?.Table + var table = entityType.GetViewOrTableMappings().SingleOrDefault(e => e.IsSplitEntityTypePrincipal ?? true)?.Table ?? entityType.GetDefaultMappings().Single().Table; if (!table.IsOptional(entityType)) { diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 5fbd0025c3e..c85a41aa11a 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -320,26 +320,26 @@ private sealed class ColumnExpressionFindingExpressionVisitor : ExpressionVisito // Always skip the table of ColumnExpression since it will traverse into deeper subquery return columnExpression; - case LeftJoinExpression leftJoinExpression: - var leftJoinTableAlias = leftJoinExpression.Table.Alias!; + case PredicateJoinExpressionBase predicateJoinExpressionBase: + var predicateJoinTableAlias = predicateJoinExpressionBase.Table.Alias!; // Visiting the join predicate will add some columns for join table. // But if all the referenced columns are in join predicate only then we can remove the join table. // So if there are no referenced columns yet means there is still potential to remove this table, // In such case we moved the columns encountered in join predicate to other dictionary and later merge // if there are more references to the join table outside of join predicate. - // We currently do this only for LeftJoin since that is the only predicate join table we remove. // We should also remove references to the outer if this column gets removed then that subquery can also remove projections - // But currently we only remove table for TPT scenario in which there are all table expressions which connects via joins. - var joinOnSameLevel = _columnReferenced!.ContainsKey(leftJoinTableAlias); - var noReferences = !joinOnSameLevel || _columnReferenced[leftJoinTableAlias] == null; - base.Visit(leftJoinExpression); + // But currently we only remove table for TPT & entity splitting scenario + // in which there are all table expressions which connects via joins. + var joinOnSameLevel = _columnReferenced!.ContainsKey(predicateJoinTableAlias); + var noReferences = !joinOnSameLevel || _columnReferenced[predicateJoinTableAlias] == null; + base.Visit(predicateJoinExpressionBase); if (noReferences && joinOnSameLevel) { - _columnsUsedInJoinCondition![leftJoinTableAlias] = _columnReferenced[leftJoinTableAlias]; - _columnReferenced[leftJoinTableAlias] = null; + _columnsUsedInJoinCondition![predicateJoinTableAlias] = _columnReferenced[predicateJoinTableAlias]; + _columnReferenced[predicateJoinTableAlias] = null; } - return leftJoinExpression; + return predicateJoinExpressionBase; default: return base.Visit(expression); @@ -930,7 +930,7 @@ private sealed class CloningExpressionVisitor : ExpressionVisitor }; newSelectExpression._mutable = selectExpression._mutable; - newSelectExpression._tptLeftJoinTables.AddRange(selectExpression._tptLeftJoinTables); + newSelectExpression._removableJoinTables.AddRange(selectExpression._removableJoinTables); foreach (var kvp in selectExpression._tpcDiscriminatorValues) { diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index da0497f7847..8ed2e6ad81a 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -45,7 +45,7 @@ public sealed partial class SelectExpression : TableExpressionBase private readonly List<(ColumnExpression Column, ValueComparer Comparer)> _identifier = new(); private readonly List<(ColumnExpression Column, ValueComparer Comparer)> _childIdentifiers = new(); - private readonly List _tptLeftJoinTables = new(); + private readonly List _removableJoinTables = new(); private readonly Dictionary)> _tpcDiscriminatorValues = new(ReferenceEqualityComparer.Instance); @@ -165,7 +165,7 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre .Aggregate((l, r) => sqlExpressionFactory.AndAlso(l, r)); var joinExpression = new LeftJoinExpression(tableExpression, joinPredicate); - _tptLeftJoinTables.Add(_tables.Count); + _removableJoinTables.Add(_tables.Count); AddTable(joinExpression, tableReferenceExpression); } @@ -187,7 +187,7 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre if (entityTypes.Length == 1) { // For single entity case, we don't need discriminator. - var table = GetTableBase(entityTypes[0]); + var table = entityTypes[0].GetViewOrTableMappings().Single().Table; var tableExpression = new TableExpression(table); var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!); @@ -212,7 +212,7 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre } else { - var tables = entityTypes.Select(e => GetTableBase(e)).ToArray(); + var tables = entityTypes.Select(e => e.GetViewOrTableMappings().Single().Table).ToArray(); var properties = GetAllPropertiesInHierarchy(entityType).ToArray(); var propertyNamesMap = new Dictionary(); for (var i = 0; i < entityTypes.Length; i++) @@ -314,39 +314,76 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre default: { // Also covers TPH - ITableBase table; - TableExpressionBase tableExpression; if (entityType.GetFunctionMappings().SingleOrDefault(e => e.IsDefaultFunctionMapping) is IFunctionMapping functionMapping) { var storeFunction = functionMapping.Table; - table = storeFunction; - tableExpression = new TableValuedFunctionExpression((IStoreFunction)storeFunction, Array.Empty()); + GenerateNonHierarchyNonSplittingEntityType( + storeFunction, new TableValuedFunctionExpression((IStoreFunction)storeFunction, Array.Empty())); } else { - table = GetTableBase(entityType); - tableExpression = new TableExpression(table); - } + var mappings = entityType.GetViewOrTableMappings().ToList(); + if (mappings.Count == 1) + { + var table = mappings[0].Table; - var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!); - AddTable(tableExpression, tableReferenceExpression); + GenerateNonHierarchyNonSplittingEntityType(table, new TableExpression(table)); + } + else + { + // entity splitting + var keyProperties = entityType.FindPrimaryKey()!.Properties; + List joinColumns = default!; + var columns = new Dictionary(); + var tableReferenceExpressionMap = new Dictionary(); + foreach (var mapping in mappings) + { + var table = mapping.Table; + var tableExpression = new TableExpression(table); + var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias); + tableReferenceExpressionMap[table] = tableReferenceExpression; - var propertyExpressions = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityType)) - { - propertyExpressions[property] = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false); - } + if (_tables.Count == 0) + { + AddTable(tableExpression, tableReferenceExpression); + joinColumns = new List(); + foreach (var property in keyProperties) + { + var columnExpression = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false); + columns[property] = columnExpression; + joinColumns.Add(columnExpression); + _identifier.Add((columnExpression, property.GetKeyValueComparer())); + } + } + else + { + var innerColumns = keyProperties.Select( + p => CreateColumnExpression(p, table, tableReferenceExpression, nullable: false)); - var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions); - _projectionMapping[new ProjectionMember()] = entityProjection; + var joinPredicate = joinColumns.Zip(innerColumns, (l, r) => sqlExpressionFactory.Equal(l, r)) + .Aggregate((l, r) => sqlExpressionFactory.AndAlso(l, r)); - var primaryKey = entityType.FindPrimaryKey(); - if (primaryKey != null) - { - foreach (var property in primaryKey.Properties) - { - _identifier.Add((propertyExpressions[property], property.GetKeyValueComparer())); + var joinExpression = new InnerJoinExpression(tableExpression, joinPredicate); + _removableJoinTables.Add(_tables.Count); + AddTable(joinExpression, tableReferenceExpression); + } + } + + foreach (var property in entityType.GetProperties()) + { + if (property.IsPrimaryKey()) + { + continue; + } + + var columnBase = mappings.Select(e => e.Table.FindColumn(property)).First(e => e != null)!; + columns[property] = CreateColumnExpression( + property, columnBase, tableReferenceExpressionMap[columnBase.Table], nullable: false); + } + + var entityProjection = new EntityProjectionExpression(entityType, columns); + _projectionMapping[new ProjectionMember()] = entityProjection; } } } @@ -354,7 +391,29 @@ internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpre break; } - static ITableBase GetTableBase(IEntityType entityType) => entityType.GetViewOrTableMappings().Single().Table; + void GenerateNonHierarchyNonSplittingEntityType(ITableBase table, TableExpressionBase tableExpression) + { + var tableReferenceExpression = new TableReferenceExpression(this, tableExpression.Alias!); + AddTable(tableExpression, tableReferenceExpression); + + var propertyExpressions = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(entityType)) + { + propertyExpressions[property] = CreateColumnExpression(property, table, tableReferenceExpression, nullable: false); + } + + var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions); + _projectionMapping[new ProjectionMember()] = entityProjection; + + var primaryKey = entityType.FindPrimaryKey(); + if (primaryKey != null) + { + foreach (var property in primaryKey.Properties) + { + _identifier.Add((propertyExpressions[property], property.GetKeyValueComparer())); + } + } + } static ITableBase GetTableBaseFiltered(IEntityType entityType, List existingTables) => entityType.GetViewOrTableMappings().Single(m => !existingTables.Contains(m.Table)).Table; @@ -1713,8 +1772,8 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi _projectionMapping.Clear(); select1._identifier.AddRange(_identifier); _identifier.Clear(); - select1._tptLeftJoinTables.AddRange(_tptLeftJoinTables); - _tptLeftJoinTables.Clear(); + select1._removableJoinTables.AddRange(_removableJoinTables); + _removableJoinTables.Clear(); foreach (var kvp in _tpcDiscriminatorValues) { select1._tpcDiscriminatorValues[kvp.Key] = kvp.Value; @@ -2902,8 +2961,8 @@ private SqlRemappingVisitor PushdownIntoSubqueryInternal() Having = null; Offset = null; Limit = null; - subquery._tptLeftJoinTables.AddRange(_tptLeftJoinTables); - _tptLeftJoinTables.Clear(); + subquery._removableJoinTables.AddRange(_removableJoinTables); + _removableJoinTables.Clear(); foreach (var kvp in _tpcDiscriminatorValues) { subquery._tpcDiscriminatorValues[kvp.Key] = kvp.Value; @@ -3213,14 +3272,17 @@ private SelectExpression Prune(IReadOnlyCollection? referencedColumns) var columnExpressionFindingExpressionVisitor = new ColumnExpressionFindingExpressionVisitor(); var columnsMap = columnExpressionFindingExpressionVisitor.FindColumns(this); var removedTableCount = 0; + // Start at 1 because we don't drop main table. + // Dropping main table is more complex because other tables need to unwrap joins to be main for (var i = 0; i < _tables.Count; i++) { var table = _tables[i]; var tableAlias = GetAliasFromTableExpressionBase(table); if (columnsMap[tableAlias] == null && (table is LeftJoinExpression - || table is OuterApplyExpression) - && _tptLeftJoinTables?.Contains(i + removedTableCount) == true) + || table is OuterApplyExpression + || table is InnerJoinExpression) // This is only valid for removable join table which are from entity splitting + && _removableJoinTables?.Contains(i + removedTableCount) == true) { _tables.RemoveAt(i); _tableReferences.RemoveAt(i); @@ -3341,7 +3403,14 @@ private static ConcreteColumnExpression CreateColumnExpression( ITableBase table, TableReferenceExpression tableExpression, bool nullable) - => new(property, table.FindColumn(property)!, tableExpression, nullable); + => CreateColumnExpression(property, table.FindColumn(property)!, tableExpression, nullable); + + private static ConcreteColumnExpression CreateColumnExpression( + IProperty property, + IColumnBase columnBase, + TableReferenceExpression tableExpression, + bool nullable) + => new(property, columnBase, tableExpression, nullable); private ConcreteColumnExpression GenerateOuterColumn( TableReferenceExpression tableReferenceExpression, @@ -3578,7 +3647,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) _usedAliases = _usedAliases, }; newSelectExpression._mutable = false; - newSelectExpression._tptLeftJoinTables.AddRange(_tptLeftJoinTables); + newSelectExpression._removableJoinTables.AddRange(_removableJoinTables); foreach (var kvp in newTpcDiscriminatorValues) { newSelectExpression._tpcDiscriminatorValues[kvp.Key] = kvp.Value; diff --git a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs index 7adb3cc2777..2efa8854f68 100644 --- a/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/EntitySplittingTestBase.cs @@ -11,10 +11,10 @@ protected EntitySplittingTestBase(ITestOutputHelper testOutputHelper) //TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - [ConditionalFact(Skip = "Entity splitting query Issue #620")] + [ConditionalFact] public virtual async Task Can_roundtrip() { - await InitializeAsync(OnModelCreating, sensitiveLogEnabled: false); + await InitializeAsync(OnModelCreating, sensitiveLogEnabled: true); await using (var context = CreateContext()) { diff --git a/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryFixtureBase.cs b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryFixtureBase.cs new file mode 100644 index 00000000000..bebe89997f3 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryFixtureBase.cs @@ -0,0 +1,84 @@ +// 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.TestModels.EntitySplitting; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class EntitySplittingQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase +{ + private SplitEntityData _expectedData; + + protected EntitySplittingQueryFixtureBase() + { + } + + protected override string StoreName { get; } = "EntitySplittingQueryTest"; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public Func GetContextCreator() => () => CreateContext(); + + public IReadOnlyDictionary GetEntityAsserters() + => new Dictionary> + { + { + typeof(SplitEntityOne), (e, a) => + { + Assert.Equal(e == null, a == null); + if (a != null) + { + var ee =(SplitEntityOne)e; + var aa =(SplitEntityOne)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.Value, aa.Value); + Assert.Equal(ee.SharedValue, aa.SharedValue); + Assert.Equal(ee.SplitValue, aa.SplitValue); + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary GetEntitySorters() + => new Dictionary> + { + { typeof(SplitEntityOne), e => ((SplitEntityOne)e)?.Id }, + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public ISetSource GetExpectedData() + { + if (_expectedData == null) + { + _expectedData = new SplitEntityData(); + } + + return _expectedData; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity(b => + { + b.ToTable("SplitEntityOneMain", tb => + { + tb.Property(e => e.SharedValue); + }); + + b.SplitToTable("SplitEntityOneOther", tb => + { + tb.Property(e => e.SharedValue).HasColumnName("OtherSharedValue"); + tb.Property(e => e.SplitValue); + }); + }); + + base.OnModelCreating(modelBuilder, context); + } + + protected override void Seed(EntitySplittingContext context) + { + var _ = GetExpectedData(); + _expectedData.Seed(context); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs new file mode 100644 index 00000000000..5e9ac7f0d98 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/EntitySplittingQueryTestBase.cs @@ -0,0 +1,33 @@ +// 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.TestModels.EntitySplitting; + +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class EntitySplittingQueryTestBase : QueryTestBase + where TFixture : EntitySplittingQueryFixtureBase, new() +{ + public EntitySplittingQueryTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Can_query_entity_which_is_split(bool async) + { + return AssertQuery( + async, + ss => ss.Set()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Can_query_entity_which_is_split_selecting_only_main_properties(bool async) + { + return AssertQuery( + async, + ss => ss.Set().Select(e => new { e.Id, e.SharedValue, e.Value })); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingContext.cs b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingContext.cs new file mode 100644 index 00000000000..3c1201a306e --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/EntitySplittingContext.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.TestModels.EntitySplitting; + +public class EntitySplittingContext : PoolableDbContext +{ + public EntitySplittingContext(DbContextOptions options) + : base(options) + { + } + + public DbSet SplitEntityOnes { get; set; } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/SplitEntityData.cs b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/SplitEntityData.cs new file mode 100644 index 00000000000..137354cea05 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/SplitEntityData.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.TestModels.EntitySplitting; + +public class SplitEntityData : ISetSource +{ + private readonly SplitEntityOne[] _splitEntityOnes; + + public SplitEntityData() + { + _splitEntityOnes = CreateSplitEntityOnes(); + } + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(SplitEntityOne)) + { + return (IQueryable)_splitEntityOnes.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + private static SplitEntityOne[] CreateSplitEntityOnes() + => new SplitEntityOne[] + { + + }; + + public void Seed(EntitySplittingContext context) + { + context.AddRange(_splitEntityOnes); + + context.SaveChanges(); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/SplitEntityOne.cs b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/SplitEntityOne.cs new file mode 100644 index 00000000000..d067f94623a --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TestModels/EntitySplitting/SplitEntityOne.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.TestModels.EntitySplitting; + +public class SplitEntityOne +{ + public int Id { get; set; } + public string Value { get; set; } + public int SharedValue { get; set; } + public string SplitValue { get; set; } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs index 73bbaf2ddd5..fad595ff441 100644 --- a/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/EntitySplittingSqlServerTest.cs @@ -10,7 +10,7 @@ public EntitySplittingSqlServerTest(ITestOutputHelper testOutputHelper) { } - [ConditionalFact(Skip = "Entity splitting query Issue #620")] + [ConditionalFact] public virtual async Task Can_roundtrip_with_triggers() { await InitializeAsync(modelBuilder => @@ -62,6 +62,33 @@ ON [MeterReadingDetails] } } + public override async Task Can_roundtrip() + { + await base.Can_roundtrip(); + + AssertSql( + @"@p0='2' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [MeterReadings] ([ReadingStatus]) +OUTPUT INSERTED.[Id] +VALUES (@p0);", + // + @"@p1='1' +@p2='100' (Size = 4000) +@p3=NULL (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [MeterReadingDetails] ([Id], [CurrentRead], [PreviousRead]) +VALUES (@p1, @p2, @p3);", + // + @"SELECT TOP(2) [m].[Id], [m0].[CurrentRead], [m0].[PreviousRead], [m].[ReadingStatus] +FROM [MeterReadings] AS [m] +INNER JOIN [MeterReadingDetails] AS [m0] ON [m].[Id] = [m0].[Id]"); + } + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs index 0a923d0fa14..89ed35d533f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsSharedTypeQuerySqlServerTest.cs @@ -4355,10 +4355,10 @@ public override async Task GroupJoin_without_DefaultIfEmpty(bool async) @"SELECT [l].[Id] FROM [Level1] AS [l] INNER JOIN ( - SELECT [t].[Level1_Optional_Id] + SELECT [l0].[Id], [l0].[Date], [l0].[Name], [t].[Id] AS [Id0], [t].[OneToOne_Required_PK_Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Level2_Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id] FROM [Level1] AS [l0] LEFT JOIN ( - SELECT [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[OneToMany_Required_Inverse2Id] + SELECT [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Level2_Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id] FROM [Level1] AS [l1] WHERE [l1].[OneToOne_Required_PK_Date] IS NOT NULL AND [l1].[Level1_Required_Id] IS NOT NULL AND [l1].[OneToMany_Required_Inverse2Id] IS NOT NULL ) AS [t] ON [l0].[Id] = CASE diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/EntitySplittingQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/EntitySplittingQuerySqlServerFixture.cs new file mode 100644 index 00000000000..c3707747859 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/EntitySplittingQuerySqlServerFixture.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class EntitySplittingQuerySqlServerFixture : EntitySplittingQueryFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/EntitySplittingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/EntitySplittingQuerySqlServerTest.cs new file mode 100644 index 00000000000..c2399a9e26b --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/EntitySplittingQuerySqlServerTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class EntitySplittingQuerySqlServerTest : EntitySplittingQueryTestBase +{ + public EntitySplittingQuerySqlServerTest(EntitySplittingQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + public override async Task Can_query_entity_which_is_split(bool async) + { + await base.Can_query_entity_which_is_split(async); + + AssertSql( + @"SELECT [s].[Id], [s].[SharedValue], [s0].[SplitValue], [s].[Value] +FROM [SplitEntityOneMain] AS [s] +INNER JOIN [SplitEntityOneOther] AS [s0] ON [s].[Id] = [s0].[Id]"); + } + + public override async Task Can_query_entity_which_is_split_selecting_only_main_properties(bool async) + { + await base.Can_query_entity_which_is_split_selecting_only_main_properties(async); + + AssertSql( + @"SELECT [s].[Id], [s].[SharedValue], [s].[Value] +FROM [SplitEntityOneMain] AS [s]"); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index fef1f7dc887..b38a1421882 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -1520,7 +1520,7 @@ public override async Task GroupBy_Aggregate_Join_converted_from_SelectMany(bool @"SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] INNER JOIN ( - SELECT [o].[CustomerID] + SELECT [o].[CustomerID], MAX([o].[OrderID]) AS [LastOrderID] FROM [Orders] AS [o] GROUP BY [o].[CustomerID] HAVING COUNT(*) > 5 @@ -1634,7 +1634,7 @@ INNER JOIN ( SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] INNER JOIN ( - SELECT [o0].[CustomerID] + SELECT [o0].[CustomerID], MAX([o0].[OrderID]) AS [LastOrderID] FROM [Orders] AS [o0] GROUP BY [o0].[CustomerID] HAVING COUNT(*) > 5 diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index 1b9f38a1369..ddbe5b40479 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -168,7 +168,7 @@ public override async Task Join_with_entity_equality_local_on_both_sources(bool SELECT [c].[CustomerID] FROM [Customers] AS [c] INNER JOIN ( - SELECT [c0].[CustomerID] + SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region] FROM [Customers] AS [c0] WHERE [c0].[CustomerID] = @__entity_equality_local_0_CustomerID ) AS [t] ON [c].[CustomerID] = [t].[CustomerID] @@ -358,12 +358,12 @@ FROM [Employees] AS [e] WHERE [e].[EmployeeID] = -1 ) AS [t] ON 1 = 1 INNER JOIN ( - SELECT [t1].[EmployeeID] + SELECT [t1].[EmployeeID], [t1].[City], [t1].[Country], [t1].[FirstName], [t1].[ReportsTo], [t1].[Title] FROM ( SELECT NULL AS [empty] ) AS [e1] LEFT JOIN ( - SELECT [e2].[EmployeeID] + SELECT [e2].[EmployeeID], [e2].[City], [e2].[Country], [e2].[FirstName], [e2].[ReportsTo], [e2].[Title] FROM [Employees] AS [e2] WHERE [e2].[EmployeeID] = -1 ) AS [t1] ON 1 = 1 diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs index db9e4df69ee..3e4a2587ed5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/SimpleQuerySqlServerTest.cs @@ -179,7 +179,10 @@ public override async Task GroupBy_aggregate_on_right_side_of_join(bool async) SELECT [o].[Id], [o].[CancellationDate], [o].[OrderId], [o].[ShippingDate] FROM [OrderItems] AS [o] INNER JOIN ( - SELECT [o0].[OrderId] AS [Key] + SELECT [o0].[OrderId] AS [Key], MAX(CASE + WHEN [o0].[ShippingDate] IS NULL AND [o0].[CancellationDate] IS NULL THEN [o0].[OrderId] + ELSE [o0].[OrderId] - 10000000 + END) AS [IsPending] FROM [OrderItems] AS [o0] WHERE [o0].[OrderId] = @__orderId_0 GROUP BY [o0].[OrderId] diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index e3f09af5148..51de11ca2c0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -8302,10 +8302,10 @@ UNION ALL FROM [Officers] AS [o] ) AS [t] INNER JOIN ( - SELECT [g0].[Nickname] + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], N'Gear' AS [Discriminator] FROM [Gears] AS [g0] UNION ALL - SELECT [o0].[Nickname] + 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 [t0] ON [t].[Nickname] = [t0].[Nickname]"); } @@ -8324,10 +8324,10 @@ UNION ALL FROM [Officers] AS [o] ) AS [t] INNER JOIN ( - SELECT [g0].[Nickname] + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], N'Gear' AS [Discriminator] FROM [Gears] AS [g0] UNION ALL - SELECT [o0].[Nickname] + 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 [t0] ON [t].[Nickname] = [t0].[Nickname]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 6e627b388af..4a32335afc1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -6943,8 +6943,11 @@ END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] INNER JOIN ( - SELECT [g0].[Nickname] + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], CASE + WHEN [o0].[Nickname] IS NOT NULL THEN N'Officer' + END AS [Discriminator] FROM [Gears] AS [g0] + LEFT JOIN [Officers] AS [o0] ON [g0].[Nickname] = [o0].[Nickname] AND [g0].[SquadId] = [o0].[SquadId] ) AS [t] ON [g].[Nickname] = [t].[Nickname]"); } @@ -6959,8 +6962,11 @@ END AS [Discriminator] FROM [Gears] AS [g] LEFT JOIN [Officers] AS [o] ON [g].[Nickname] = [o].[Nickname] AND [g].[SquadId] = [o].[SquadId] INNER JOIN ( - SELECT [g0].[Nickname] + SELECT [g0].[Nickname], [g0].[SquadId], [g0].[AssignedCityName], [g0].[CityOfBirthName], [g0].[FullName], [g0].[HasSoulPatch], [g0].[LeaderNickname], [g0].[LeaderSquadId], [g0].[Rank], CASE + WHEN [o0].[Nickname] IS NOT NULL THEN N'Officer' + END AS [Discriminator] FROM [Gears] AS [g0] + LEFT JOIN [Officers] AS [o0] ON [g0].[Nickname] = [o0].[Nickname] AND [g0].[SquadId] = [o0].[SquadId] ) AS [t] ON [g].[Nickname] = [t].[Nickname]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs index 6a1e057e419..c466e82aceb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs @@ -596,7 +596,7 @@ public override void QF_Select_Correlated_Subquery_In_Anonymous_Nested_With_QF() @"SELECT [o].[CustomerId], [o].[OrderDate] FROM [Orders] AS [o] INNER JOIN ( - SELECT [g].[OrderId] + SELECT [c].[Id], [c].[FirstName], [c].[LastName], [g].[OrderId], [g].[CustomerId], [g].[OrderDate] FROM [Customers] AS [c] CROSS APPLY [dbo].[GetOrdersWithMultipleProducts]([c].[Id]) AS [g] ) AS [t] ON [o].[Id] = [t].[OrderId]"); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/EntitySplittingQuerySqliteFixture.cs b/test/EFCore.Sqlite.FunctionalTests/Query/EntitySplittingQuerySqliteFixture.cs new file mode 100644 index 00000000000..39cc26d50ce --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/EntitySplittingQuerySqliteFixture.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class EntitySplittingQuerySqliteFixture : EntitySplittingQueryFixtureBase +{ + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/EntitySplittingQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/EntitySplittingQuerySqliteTest.cs new file mode 100644 index 00000000000..7292d06b522 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/EntitySplittingQuerySqliteTest.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query; + +public class EntitySplittingQuerySqliteTest : EntitySplittingQueryTestBase +{ + public EntitySplittingQuerySqliteTest(EntitySplittingQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}