From b72254b27a0551696e858d563c9648a955f368f1 Mon Sep 17 00:00:00 2001 From: Nikita Grishin Date: Fri, 24 May 2024 07:46:18 +0300 Subject: [PATCH] #205 --- .../Translation/ColumnExpressionTranslator.cs | 2 +- .../ColumnsChainExpressionTranslator.cs | 3 +- .../Translation/RenameExpressionTranslator.cs | 3 +- .../DataAccess.Orm.Sql.csproj | 3 + .../Linq/SqlCommandMaterializer.cs | 72 ++---- .../Translation/AllLinqExpressionVisitor.cs | 4 +- .../AnyMethodCallExpressionTranslator.cs | 2 +- .../Translation/CountLinqExpressionVisitor.cs | 2 +- .../Expressions/ColumnExpression.cs | 19 +- .../Expressions/ColumnsChainExpression.cs | 16 +- .../Expressions/DeleteExpression.cs | 2 +- .../Expressions/FilterExpression.cs | 2 +- .../Expressions/InsertExpression.cs | 2 +- .../Translation/Expressions/JoinExpression.cs | 8 + .../Expressions/NamedSourceExpression.cs | 7 +- .../Translation/Expressions/NewExpression.cs | 2 +- .../Expressions/ProjectionExpression.cs | 2 +- .../Expressions/QuerySourceExpression.cs | 2 +- .../Expressions/RenameExpression.cs | 12 +- .../Expressions/UpdateExpression.cs | 2 +- .../OrderByLinqExpressionVisitor.cs | 41 +++- .../Translation/RelationsExpressionVisitor.cs | 46 ++++ .../RepositoryAllLinqExpressionVisitor.cs | 83 ++----- .../SelectLinqExpressionVisitor.cs | 147 +++++------ .../Translation/TranslationContext.cs | 52 ++-- .../TranslationExpressionVisitor.cs | 63 ++++- .../Translation/WhereLinqExpressionVisitor.cs | 231 ++++++++++++------ .../Tests/GenericHost.Test/LinqToSqlTests.cs | 82 +++++-- 28 files changed, 524 insertions(+), 388 deletions(-) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/RelationsExpressionVisitor.cs diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs index 968c7bcf..9f219eee 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs @@ -38,7 +38,7 @@ public string Translate(ColumnExpression expression, int depth) } sb.Append('"'); - sb.Append(expression.Name); + sb.Append(expression.Member.Name); sb.Append('"'); return sb.ToString(); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs index b0fe8106..743d6021 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs @@ -1,6 +1,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Postgres.Translation { using System; + using System.Linq; using System.Text; using AutoRegistration.Api.Abstractions; using AutoRegistration.Api.Attributes; @@ -38,7 +39,7 @@ public string Translate(ColumnsChainExpression expression, int depth) } sb.Append('"'); - sb.Append(expression.Name); + sb.Append(string.Join("_", expression.Members.Select(member => member.Name))); sb.Append('"'); return sb.ToString(); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/RenameExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/RenameExpressionTranslator.cs index 3d62e5f8..d101cab5 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/RenameExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/RenameExpressionTranslator.cs @@ -1,6 +1,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Postgres.Translation { using System; + using System.Linq; using System.Text; using AutoRegistration.Api.Abstractions; using AutoRegistration.Api.Attributes; @@ -35,7 +36,7 @@ public string Translate(RenameExpression expression, int depth) sb.Append(" AS "); sb.Append('"'); - sb.Append(expression.Name); + sb.Append(string.Join("_", expression.Members.Select(member => member.Name))); sb.Append('"'); return sb.ToString(); diff --git a/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj b/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj index c8bfe0c4..ebe125ee 100644 --- a/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj +++ b/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj @@ -414,5 +414,8 @@ ISqlExpression.cs + + TranslationExpressionVisitor.cs + diff --git a/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs b/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs index 1e74ce8e..907e746a 100644 --- a/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs +++ b/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs @@ -87,7 +87,7 @@ private async IAsyncEnumerable MaterializeInternal( IDictionary values, CancellationToken token) { - var relationValues = InitializeRelations(type, values); + MaterializeRelations(transaction, type, values); var multipleRelationValues = InitializeMultipleRelations(type, values); @@ -96,7 +96,7 @@ private async IAsyncEnumerable MaterializeInternal( object? built; if (type.IsDatabaseEntity() - && TryGetValueFromTransaction(transaction, type, values[nameof(IUniqueIdentified.PrimaryKey)] !, out var stored)) + && TryGetValueFromTransaction(transaction, type, arrangedValues[nameof(IUniqueIdentified.PrimaryKey)] !, out var stored)) { // TODO: why refill from query results? _objectBuilder.Fill(type, stored, arrangedValues); @@ -109,8 +109,6 @@ private async IAsyncEnumerable MaterializeInternal( StoreInTransaction(transaction, built); - MaterializeRelations(transaction, built, relationValues); - await MaterializeMultipleRelations(transaction, built, type, multipleRelationValues, token).ConfigureAwait(false); return built; @@ -280,65 +278,39 @@ private static void StoreInTransaction( } } - private IReadOnlyDictionary Values)> InitializeRelations( + private void MaterializeRelations( + IAdvancedDatabaseTransaction transaction, Type type, IDictionary values) { - return _modelProvider + var relations = _modelProvider .Columns(type) .Values - .Where(column => column.IsRelation) - .ToDictionary( - column => column, - column => - { - /* - * initialize database entity with null relation and set it later - */ - - var primaryKey = values[column.Name]; - - values[column.Name] = column.Relation.Target.DefaultValue(); - - var relationKeys = values - .Select(pair => pair.Key) - .Where(key => key.StartsWith($"{column.Name}_")) - .ToList(); - - var relationValues = new Dictionary(); + .Where(column => column.IsRelation); - foreach (var relationKey in relationKeys) - { - if (values.Remove(relationKey, out var value)) - { - var cleanRelationKey = relationKey.Substring(column.Name.Length + 1); - relationValues[cleanRelationKey] = value; - } - } + foreach (var column in relations) + { + var primaryKey = values[column.Name]; - return (primaryKey, (IDictionary)relationValues); - }); - } + var relationKeys = values + .Select(pair => pair.Key) + .Where(key => key.StartsWith($"{column.Name}_")) + .ToList(); - private void MaterializeRelations( - IAdvancedDatabaseTransaction transaction, - object? built, - IReadOnlyDictionary)> relationValues) - { - foreach (var (column, pair) in relationValues) - { - var (primaryKey, arrangedValues) = pair; + var relationValues = new Dictionary(); - if (primaryKey is null or DBNull) + foreach (var relationKey in relationKeys) { - continue; + if (values.Remove(relationKey, out var value)) + { + var cleanRelationKey = relationKey.Substring(column.Name.Length + 1); + relationValues[cleanRelationKey] = value; + } } - var relation = column.Table.IsMtmTable + values[column.Name] = column.Table.IsMtmTable ? primaryKey - : MaterializeRelation(transaction, column.Relation.Target, primaryKey, arrangedValues); - - column.Relation.Property.Declared.SetValue(built, relation); + : MaterializeRelation(transaction, column.Relation.Target, primaryKey!, relationValues); } } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs index b7f37e68..28d5b8d5 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs @@ -38,7 +38,7 @@ public bool TryVisit( var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); var source = new NamedSourceExpression(itemType, context.SqlExpression, parameterExpression); - using (context.OpenParameterScope(parameterExpression)) + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); } @@ -54,7 +54,7 @@ public bool TryVisit( var right = new Expressions.MethodCallExpression(typeof(int), nameof(Queryable.Count), null, new[] { new StarExpression() }); var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Equal, left, right); var parenthesesExpression = new ParenthesesExpression(binaryExpression); - var renameExpression = new RenameExpression(typeof(bool), method.Name, parenthesesExpression); + var renameExpression = new RenameExpression(typeof(bool), new[] { method }, parenthesesExpression); var projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }, null, null); context.Remember(projectionExpression); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs index 16b1fbe6..3efae77a 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs @@ -44,7 +44,7 @@ public bool TryVisit( var right = new QueryParameterExpression(typeof(int), name); var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.GreaterThan, left, right); var parenthesesExpression = new ParenthesesExpression(binaryExpression); - var renameExpression = new RenameExpression(typeof(bool), method.Name, parenthesesExpression); + var renameExpression = new RenameExpression(typeof(bool), new[] { method }, parenthesesExpression); var projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }, null, null); context.Remember(projectionExpression); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs index 9feee935..e9dde234 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs @@ -37,7 +37,7 @@ public bool TryVisit( var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); var source = new NamedSourceExpression(itemType, new ParenthesesExpression(context.SqlExpression), parameterExpression); var countAllMethodCall = new Expressions.MethodCallExpression(typeof(int), nameof(Queryable.Count), null, new[] { new StarExpression() }); - var renameExpression = new RenameExpression(typeof(int), method.Name, countAllMethodCall); + var renameExpression = new RenameExpression(typeof(int), new[] { method }, countAllMethodCall); var projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }, null, null); context.Remember(projectionExpression); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs index c16cde0e..48a745c4 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs @@ -8,46 +8,35 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions /// public class ColumnExpression : ISqlExpression { - private readonly string? _nameOverride; - /// .cctor /// Type /// MemberInfo /// Source - /// nameOverride public ColumnExpression( Type type, MemberInfo member, - ISqlExpression source, - string? nameOverride = null) + ISqlExpression source) { if (source is not ParameterExpression) { throw new ArgumentException($"{nameof(ColumnExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); } - _nameOverride = nameOverride; - - Member = member; Type = type; + Member = member; Source = source; } /// - /// Name + /// Type /// - public string Name => _nameOverride ?? Member.Name; + public Type Type { get; } /// /// Member /// public MemberInfo Member { get; } - /// - /// Type - /// - public Type Type { get; } - /// /// Source /// diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs index deb63372..ecfcfbbe 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs @@ -3,7 +3,6 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions using System; using System.Collections.Generic; using System.Reflection; - using Basics; /// /// ColumnsChainExpression @@ -12,7 +11,7 @@ public class ColumnsChainExpression : ISqlExpression { /// .cctor /// Type - /// MemberInfos + /// MemberInfo /// Source public ColumnsChainExpression( Type type, @@ -24,26 +23,21 @@ public ColumnsChainExpression( throw new ArgumentException($"{nameof(ColumnsChainExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); } - Members = members; Type = type; + Members = members; Source = source; } /// - /// Name + /// Type /// - public string Name => Members.ToString("_", member => member.Name); + public Type Type { get; } /// - /// Member + /// Members /// public IReadOnlyCollection Members { get; } - /// - /// Type - /// - public Type Type { get; } - /// /// Source /// diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs index 56fa2515..15fcdf2b 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs @@ -19,7 +19,7 @@ public DeleteExpression( } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs index 33c5ba05..c6435821 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs @@ -34,7 +34,7 @@ public FilterExpression( } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs index 6c3414d1..711ad94a 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs @@ -25,7 +25,7 @@ public InsertExpression( } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JoinExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JoinExpression.cs index 2403c0a7..a1a52736 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JoinExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JoinExpression.cs @@ -8,10 +8,12 @@ public class JoinExpression : ISqlExpression { /// .cctor + /// ItemType /// Left source expression /// Right source expression /// On expression public JoinExpression( + Type itemType, ISqlExpression leftSource, ISqlExpression rightSource, BinaryExpression on) @@ -28,11 +30,17 @@ public JoinExpression( throw new ArgumentException($"{nameof(JoinExpression)} doesn't support {rightSource.GetType().Name} as {nameof(rightSource)} argument"); } + ItemType = itemType; LeftSource = leftSource; RightSource = rightSource; On = on; } + /// + /// ItemType + /// + public Type ItemType { get; } + /// /// Left source expression /// diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs index cd63c77f..126a88c3 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs @@ -16,10 +16,7 @@ public NamedSourceExpression( ISqlExpression source, ParameterExpression parameter) { - if (source is not JoinExpression - && source is not OrderByExpression - && source is not ParenthesesExpression - && source is not ProjectionExpression + if (source is not ParenthesesExpression && source is not QuerySourceExpression) { throw new ArgumentException($"{nameof(NamedSourceExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); @@ -31,7 +28,7 @@ public NamedSourceExpression( } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs index 0d788adb..df8c13dc 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs @@ -18,7 +18,7 @@ public NewExpression(Type itemType, IReadOnlyCollection paramete } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs index 35a7f1e4..bf12c8db 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs @@ -35,7 +35,7 @@ public ProjectionExpression( } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/QuerySourceExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/QuerySourceExpression.cs index 6d6f99ea..030d4628 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/QuerySourceExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/QuerySourceExpression.cs @@ -15,7 +15,7 @@ public QuerySourceExpression(Type itemType) } /// - /// Type + /// ItemType /// public Type ItemType { get; } } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs index ac1c8102..452ba13e 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs @@ -1,6 +1,8 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions { using System; + using System.Collections.Generic; + using System.Reflection; /// /// RenameExpression @@ -9,11 +11,11 @@ public class RenameExpression : ISqlExpression { /// .cctor /// Type - /// Name + /// MemberInfo /// Source expression public RenameExpression( Type type, - string name, + IReadOnlyCollection members, ISqlExpression source) { if (source is not BinaryExpression @@ -32,7 +34,7 @@ public RenameExpression( } Type = type; - Name = name; + Members = members; Source = source; } @@ -42,9 +44,9 @@ public RenameExpression( public Type Type { get; } /// - /// Name + /// Members /// - public string Name { get; } + public IReadOnlyCollection Members { get; } /// /// Source diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs index c4987ef9..2b016f8a 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs @@ -23,7 +23,7 @@ public UpdateExpression( } /// - /// Type + /// ItemType /// public Type ItemType { get; } diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs index efae6f89..972383c8 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs @@ -27,9 +27,7 @@ public bool TryVisit( var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); if (method == LinqMethods.QueryableOrderBy() - || method == LinqMethods.QueryableOrderByDescending() - || method == LinqMethods.QueryableThenBy() - || method == LinqMethods.QueryableThenByDescending()) + || method == LinqMethods.QueryableOrderByDescending()) { visitor.Visit(methodCallExpression.Arguments[0]); var source = context.SqlExpression; @@ -39,19 +37,48 @@ public bool TryVisit( var parameterExpression = namedSourceExpression.Parameter; ISqlExpression orderByExpressionAccessor; - using (context.OpenParameterScope(parameterExpression)) + using (context.OpenParametersScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + orderByExpressionAccessor = context.SqlExpression; + } + + var direction = method == LinqMethods.QueryableOrderBy() + ? EnOrderingDirection.Asc + : EnOrderingDirection.Desc; + + projectionExpression.OrderByExpression = new OrderByExpression(new List()); + var orderByExpressionExpression = new OrderByExpressionExpression(orderByExpressionAccessor, direction); + ((ICollection)projectionExpression.OrderByExpression.Expressions).Add(orderByExpressionExpression); + context.Remember(projectionExpression); + + return true; + } + } + + if (method == LinqMethods.QueryableThenBy() + || method == LinqMethods.QueryableThenByDescending()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var source = context.SqlExpression; + + if (source is ProjectionExpression { Source: NamedSourceExpression namedSourceExpression, OrderByExpression: { } orderByExpression } projectionExpression) + { + var parameterExpression = namedSourceExpression.Parameter; + ISqlExpression orderByExpressionAccessor; + + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); orderByExpressionAccessor = context.SqlExpression; } - var direction = method == LinqMethods.QueryableOrderBy() || method == LinqMethods.QueryableThenBy() + var direction = method == LinqMethods.QueryableThenBy() ? EnOrderingDirection.Asc : EnOrderingDirection.Desc; var orderByExpressionExpression = new OrderByExpressionExpression(orderByExpressionAccessor, direction); - projectionExpression.OrderByExpression ??= new OrderByExpression(new List()); - ((ICollection)projectionExpression.OrderByExpression.Expressions).Add(orderByExpressionExpression); + ((ICollection)orderByExpression.Expressions).Add(orderByExpressionExpression); context.Remember(projectionExpression); return true; diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/RelationsExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/RelationsExpressionVisitor.cs new file mode 100644 index 00000000..a5eb372c --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/RelationsExpressionVisitor.cs @@ -0,0 +1,46 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Reflection; + using Basics; + using Model; + + internal class RelationsExpressionVisitor : ExpressionVisitor + { + private readonly IModelProvider _modelProvider; + private readonly ICollection _relations; + + private RelationsExpressionVisitor( + IModelProvider modelProvider, + ICollection relations) + { + _modelProvider = modelProvider; + _relations = relations; + } + + public static IReadOnlyCollection ExtractRelations( + IModelProvider modelProvider, + Expression expression) + { + var relations = new HashSet(); + var visitor = new RelationsExpressionVisitor(modelProvider, relations); + _ = visitor.Visit(expression); + return relations; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Member is PropertyInfo propertyInfo + && propertyInfo.PropertyType.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>)) + && _modelProvider.Columns(propertyInfo.ReflectedType!).TryGetValue(propertyInfo.Name, out var columnInfo) + && columnInfo.Relation != null) + { + _relations.Add(columnInfo.Relation); + } + + return base.VisitMember(node); + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/RepositoryAllLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/RepositoryAllLinqExpressionVisitor.cs index 6e75f459..320160f3 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/RepositoryAllLinqExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/RepositoryAllLinqExpressionVisitor.cs @@ -42,33 +42,40 @@ public bool TryVisit( if (context.IsOuterExpression() && !itemType.IsSqlView()) { var querySourceExpression = new QuerySourceExpression(itemType); + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + var namedSourceExpression = new NamedSourceExpression(itemType, querySourceExpression, parameterExpression); + var expressions = SelectLinqExpressionVisitor + .SelectAll(_modelProvider, itemType, parameterExpression) + .Cast() + .ToList(); - if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, querySourceExpression) is { } joinProjectionExpression) + IReadOnlyCollection relations = itemType.IsMtmTable() + ? Array.Empty() + : _modelProvider + .Columns(itemType) + .Select(it => it.Value) + .Where(column => column.IsRelation) + .Select(column => column.Relation!) + .ToList(); + + ProjectionExpression projectionExpression; + + if (SelectLinqExpressionVisitor.TryBuildJoinExpression(context, _modelProvider, namedSourceExpression, relations, out var joinExpression, out var relationExpressions, out _)) { - var projectionExpression = joinProjectionExpression; - context.Remember(projectionExpression); + expressions = expressions.Concat(relationExpressions).ToList(); + projectionExpression = new ProjectionExpression(itemType, joinExpression, expressions, null, null); } else { - var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); - var namedSourceExpression = new NamedSourceExpression(itemType, querySourceExpression, parameterExpression); - var expressions = SelectAll(_modelProvider, itemType, parameterExpression); - var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, null, null); - context.Remember(projectionExpression); + projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, null, null); } + + context.Remember(projectionExpression); } else { var querySourceExpression = new QuerySourceExpression(itemType); - - if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, querySourceExpression) is { } projectionExpression) - { - context.Remember(projectionExpression); - } - else - { - context.Remember(querySourceExpression); - } + context.Remember(querySourceExpression); } return true; @@ -76,47 +83,5 @@ public bool TryVisit( return false; } - - internal static IReadOnlyCollection SelectAll( - IModelProvider modelProvider, - Type type, - Expressions.ParameterExpression parameterExpression) - { - if (!type.IsClass - || type.IsPrimitive() - || type.IsCollection()) - { - throw new InvalidOperationException(nameof(SelectAll)); - } - - return modelProvider - .Columns(type) - .Values - .Where(column => !column.IsMultipleRelation) - .Select(column => column.BuildExpression(parameterExpression)) - .ToList(); - - /*.return columns - .Select(column => column.BuildExpression(parameterExpression)) - - // TODO: do we need to select relations? - Concat(columns - .Where(column => column.IsRelation - && !modelProvider.Tables[column.Relation.Target].IsMtmTable) - .SelectMany(column => modelProvider - .Columns(column.Relation.Target) - .Values - .Where(targetColumn => !targetColumn.IsMultipleRelation) - .Select(targetColumn => (column, targetColumn))) - .Select(pair => - { - var (column, targetColumn) = pair; - - var targetParameterExpression = new Expressions.ParameterExpression(targetColumn.Table.Type, parameterExpression.Name); - var columnExpression = targetColumn.BuildExpression(targetParameterExpression); - return (ISqlExpression)new RenameExpression(columnExpression.Type, $"{column.Relation.Property.Reflected.Name}_{columnExpression.Name}", columnExpression); - })) - .ToList();*/ - } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/SelectLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/SelectLinqExpressionVisitor.cs index 1565e3a0..aa8b8385 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/SelectLinqExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/SelectLinqExpressionVisitor.cs @@ -2,6 +2,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using AutoRegistration.Api.Abstractions; @@ -43,12 +44,12 @@ public bool TryVisit( var source = context.SqlExpression; var sourceItemType = GetSourceItemType(source); var parameterExpression = new Expressions.ParameterExpression(sourceItemType, context.NextLambdaParameterName()); - var unnamedSource = source is QuerySourceExpression ? source : new ParenthesesExpression(context.SqlExpression); + var unnamedSource = source is QuerySourceExpression ? source : new ParenthesesExpression(source); var namedSourceExpression = new NamedSourceExpression(sourceItemType, unnamedSource, parameterExpression); IReadOnlyCollection expressions; - using (context.OpenParameterScope(namedSourceExpression.Parameter)) + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); expressions = context.SqlExpression is Expressions.NewExpression newExpression @@ -56,31 +57,18 @@ public bool TryVisit( : new[] { context.SqlExpression }; } - if (expressions.Count == 1 - && expressions.Single() is ColumnExpression columnExpression - && columnExpression.Type.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>))) - { - /* - * handle relation selector - */ - - // TODO: try to remove - /*var relationParameterExpression = new Expressions.ParameterExpression(columnExpression.Type, namedSourceExpression.Parameter.Name); - expressions = RepositoryAllLinqExpressionVisitor - .SelectAll(_modelProvider, columnExpression.Type, relationParameterExpression) - .Select(it => - { - var relationColumnExpression = new ColumnExpression(it.Type, it.Member, it.Source, $"{columnExpression.Name}_{it.Name}"); - return new RenameExpression(it.Type, it.Name, relationColumnExpression); - }) - .ToList();*/ - } + var relations = RelationsExpressionVisitor.ExtractRelations(_modelProvider, methodCallExpression.Arguments[1]); - var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, null, null); + ProjectionExpression projectionExpression; - if (BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + if (TryBuildJoinExpression(context, _modelProvider, namedSourceExpression, relations, out var joinExpression, out var relationExpressions, out _)) + { + expressions = expressions.Concat(relationExpressions).ToList(); + projectionExpression = new ProjectionExpression(itemType, joinExpression, expressions, null, null); + } + else { - projectionExpression = joinProjectionExpression; + projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, null, null); } context.Remember(projectionExpression); @@ -91,7 +79,6 @@ public bool TryVisit( return false; } - // TODO: private internal static ISqlExpression? GetSource(ISqlExpression source) { return source switch @@ -106,7 +93,6 @@ public bool TryVisit( }; } - // TODO: private internal static Type GetSourceItemType(ISqlExpression source) { return source switch @@ -120,96 +106,73 @@ internal static Type GetSourceItemType(ISqlExpression source) }; } - // TODO: private - internal static ProjectionExpression? BuildJoinExpression( - TranslationContext context, + internal static IReadOnlyCollection SelectAll( IModelProvider modelProvider, - ISqlExpression sourceExpression) + Type type, + Expressions.ParameterExpression parameterExpression) { - var sourceItemType = GetSourceItemType(sourceExpression); - - if (!sourceItemType.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>))) + if (!type.IsClass + || type.IsPrimitive() + || type.IsCollection()) { - return null; + throw new InvalidOperationException(nameof(SelectAll)); } - var relations = modelProvider - .Tables[sourceItemType] - .Columns - .Select(it => it.Value) - .Where(column => column.IsRelation) - .Select(column => column.Relation) + return modelProvider + .Columns(type) + .Values + .Where(column => !column.IsMultipleRelation) + .Select(column => column.BuildExpression(parameterExpression)) .ToList(); + } + internal static bool TryBuildJoinExpression( + TranslationContext context, + IModelProvider modelProvider, + NamedSourceExpression sourceExpression, + IReadOnlyCollection relations, + [NotNullWhen(true)] out JoinExpression? joinExpression, + [NotNullWhen(true)] out IReadOnlyCollection? relationExpressions, + [NotNullWhen(true)] out IReadOnlyCollection? joinParameterExpressions) + { if (!relations.Any()) { - return null; + joinExpression = null; + relationExpressions = null; + joinParameterExpressions = null; + return false; } - var sourceParameterExpression = new Expressions.ParameterExpression(sourceItemType, context.NextLambdaParameterName()); - NamedSourceExpression sourceNamedSourceExpression; - IReadOnlyCollection sourceExpressions; - FilterExpression? filterExpression; - OrderByExpression? orderByExpression; - - switch (sourceExpression) + ISqlExpression joinAccumulator = sourceExpression; + IEnumerable expressions = Array.Empty(); + var parameterExpressions = new List { - case ProjectionExpression sourceProjectionExpression: - { - filterExpression = sourceProjectionExpression.FilterExpression != null - ? new FilterExpression(sourceProjectionExpression.FilterExpression.ItemType, WhereLinqExpressionVisitor.ReplaceParameterSqlExpressionVisitor.Replace(sourceProjectionExpression.FilterExpression.Predicate, sourceParameterExpression)) - : null; - sourceProjectionExpression.FilterExpression = null; - orderByExpression = sourceProjectionExpression.OrderByExpression != null - ? new OrderByExpression(sourceProjectionExpression.OrderByExpression.Expressions.Select(orderByExpressionExpression => (OrderByExpressionExpression)WhereLinqExpressionVisitor.ReplaceParameterSqlExpressionVisitor.Replace(orderByExpressionExpression, sourceParameterExpression)).ToList()) - : null; - sourceProjectionExpression.OrderByExpression = null; - var sourceParenthesesExpression = new ParenthesesExpression(sourceProjectionExpression); - sourceNamedSourceExpression = new NamedSourceExpression(sourceItemType, sourceParenthesesExpression, sourceParameterExpression); - sourceExpressions = RepositoryAllLinqExpressionVisitor.SelectAll(modelProvider, sourceItemType, sourceParameterExpression); - - break; - } - - case QuerySourceExpression querySourceExpression: - { - filterExpression = null; - orderByExpression = null; - sourceNamedSourceExpression = new NamedSourceExpression(sourceItemType, querySourceExpression, sourceParameterExpression); - sourceExpressions = RepositoryAllLinqExpressionVisitor.SelectAll(modelProvider, sourceItemType, sourceParameterExpression); - break; - } - - default: - { - throw new NotSupportedException(sourceExpression.GetType().FullName); - } - } - - ISqlExpression joinAccumulator = sourceNamedSourceExpression; - IEnumerable expressions = sourceExpressions.ToList(); + sourceExpression.Parameter + }; foreach (var relation in relations) { var targetItemType = relation.Target; var targetQuerySourceExpression = new QuerySourceExpression(targetItemType); var targetParameterExpression = new Expressions.ParameterExpression(targetItemType, context.NextLambdaParameterName()); + parameterExpressions.Add(targetParameterExpression); var targetNamedSourceExpression = new NamedSourceExpression(targetItemType, targetQuerySourceExpression, targetParameterExpression); - var targetExpressions = RepositoryAllLinqExpressionVisitor - .SelectAll(modelProvider, targetItemType, targetParameterExpression) - .Select(sqlExpression => sqlExpression is ColumnExpression columnExpression - ? new RenameExpression(columnExpression.Type, $"{relation.Property.Reflected.Name}_{columnExpression.Name}", columnExpression) - : sqlExpression) + var targetExpressions = SelectAll(modelProvider, targetItemType, targetParameterExpression) + .Select(columnExpression => new RenameExpression( + columnExpression.Type, + new[] { relation.Property.Reflected, columnExpression.Member }, + columnExpression)) .ToList(); - var onExpression = BuildJoinOnExpression(modelProvider, relation, sourceParameterExpression, targetParameterExpression); - joinAccumulator = new JoinExpression(joinAccumulator, targetNamedSourceExpression, onExpression); + var onExpression = BuildJoinOnExpression(modelProvider, relation, sourceExpression.Parameter, targetParameterExpression); + joinAccumulator = new JoinExpression(sourceExpression.ItemType, joinAccumulator, targetNamedSourceExpression, onExpression); expressions = expressions.Concat(targetExpressions); } - var projectionExpression = new ProjectionExpression(null!, joinAccumulator, expressions.ToList(), filterExpression, orderByExpression); - - return projectionExpression; + joinExpression = (JoinExpression)joinAccumulator; + relationExpressions = expressions.ToList(); + joinParameterExpressions = parameterExpressions; + return true; static Expressions.BinaryExpression BuildJoinOnExpression( IModelProvider modelProvider, diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/TranslationContext.cs b/src/Modules/DataAccess.Orm.Sql/Translation/TranslationContext.cs index 8c92175b..7ece68c8 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/TranslationContext.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/TranslationContext.cs @@ -20,7 +20,6 @@ public class TranslationContext : ICloneable public const string CommandParameterFormat = "param_{0}"; private Dictionary> _extractors; - private Stack _path; private ISqlExpression? _sqlExpression; private int _commandParameterIndex; @@ -29,8 +28,10 @@ public class TranslationContext : ICloneable /// .cctor internal TranslationContext() { + ParameterExpressions = new List(); + _extractors = new Dictionary>(); - _path = new Stack(); + Path = new Stack(); _sqlExpression = null; _commandParameterIndex = 0; @@ -40,10 +41,15 @@ internal TranslationContext() /// /// ParameterExpression /// - public Expressions.ParameterExpression? ParameterExpression { get; private set; } + public IReadOnlyCollection ParameterExpressions { get; private set; } internal ISqlExpression SqlExpression => _sqlExpression ?? throw new InvalidOperationException("sql expression is empty"); + /// + /// _path + /// + internal Stack Path { get; set; } // TODO: + /// Remember /// ISqlExpression public void Remember(ISqlExpression expression) @@ -57,7 +63,7 @@ public TranslationContext Clone() return new TranslationContext { _extractors = _extractors, - _path = _path, + Path = Path, _commandParameterIndex = _commandParameterIndex, _lambdaParameterIndex = 0 @@ -73,25 +79,35 @@ object ICloneable.Clone() /// /// OpenParameterScope /// - /// ParameterExpression + /// ParameterExpression /// Opened scope - public IDisposable OpenParameterScope(Expressions.ParameterExpression parameter) + public IDisposable OpenParametersScope(IReadOnlyCollection parameters) { - var previous = ParameterExpression; + var previous = ParameterExpressions; - return Disposable.Create(parameter, Open, Close); + return Disposable.Create(parameters, Open, Close); - void Open(Expressions.ParameterExpression parameterExpression) + void Open(IReadOnlyCollection parameterExpression) { - ParameterExpression = parameterExpression; + ParameterExpressions = parameterExpression; } - void Close(Expressions.ParameterExpression parameterExpression) + void Close(IReadOnlyCollection parameterExpression) { - ParameterExpression = previous; + ParameterExpressions = previous; } } + /// + /// OpenParameterScope + /// + /// ParameterExpression + /// Opened scope + public IDisposable OpenParametersScope(Expressions.ParameterExpression parameter) + { + return OpenParametersScope(new[] { parameter }); + } + /// /// Gets next command parameter name /// @@ -125,7 +141,7 @@ internal void CaptureCommandParameterExtractor( throw new InvalidOperationException($"command parameter {parameterName} have already been captured"); } - _extractors[parameterName] = extractor ?? CommandParameterExtractionContext.GenerateCommandParameterExtractor(parameterName, _path.Reverse().ToArray()); + _extractors[parameterName] = extractor ?? CommandParameterExtractionContext.GenerateCommandParameterExtractor(parameterName, Path.Reverse().ToArray()); } internal DisposableAction WithinPathScope( @@ -136,13 +152,13 @@ internal DisposableAction WithinPathScope( internal bool IsOuterExpression() { - if (_path.Count == 1) + if (Path.Count == 1) { return true; } - if (_path.Count == 2 - && _path.Last() is System.Linq.Expressions.MethodCallExpression expression) + if (Path.Count == 2 + && Path.Last() is System.Linq.Expressions.MethodCallExpression expression) { var method = expression.Method.GenericMethodDefinitionOrSelf(); @@ -164,12 +180,12 @@ internal bool IsOuterExpression() private void PushPath(Expression expression) { - _path.Push(expression); + Path.Push(expression); } private void PopPath(Expression expression) { - _ = _path.Pop(); + _ = Path.Pop(); } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs index eab4d2ee..019b84c1 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs @@ -10,6 +10,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using AutoRegistration.Api.Enumerations; using Basics; using Expressions; + using Model; using BinaryExpression = System.Linq.Expressions.BinaryExpression; using ConditionalExpression = System.Linq.Expressions.ConditionalExpression; using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; @@ -21,19 +22,23 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation /// TranslationExpressionVisitor /// [Component(EnLifestyle.Singleton)] - public class TranslationExpressionVisitor : ExpressionVisitor, + internal class TranslationExpressionVisitor : ExpressionVisitor, // TODO: visibility (make public) IResolvable { + private readonly IModelProvider _modelProvider; private readonly ILinqExpressionPreprocessorComposite _preprocessor; private readonly IEnumerable _linqExpressionVisitors; /// .cctor + /// IModelProvider /// ILinqExpressionPreprocessorComposite /// ILinqExpressionVisitor public TranslationExpressionVisitor( + IModelProvider modelProvider, ILinqExpressionPreprocessorComposite preprocessor, IEnumerable linqExpressionVisitors) { + _modelProvider = modelProvider; _preprocessor = preprocessor; _linqExpressionVisitors = linqExpressionVisitors; } @@ -48,22 +53,25 @@ public SqlExpression Translate( TranslationContext context, Expression expression) { - var visitor = new InternalTranslationExpressionVisitor(this, context, expression); + var visitor = new InternalTranslationExpressionVisitor(_modelProvider, this, context, expression); return visitor.Translate(); } private class InternalTranslationExpressionVisitor : ExpressionVisitor { + private readonly IModelProvider _modelProvider; private readonly TranslationExpressionVisitor _visitor; private readonly TranslationContext _context; private readonly Expression _expression; internal InternalTranslationExpressionVisitor( + IModelProvider modelProvider, TranslationExpressionVisitor visitor, TranslationContext context, Expression expression) { + _modelProvider = modelProvider; _visitor = visitor; _context = context; _expression = expression; @@ -108,11 +116,17 @@ protected override Expression VisitMember(MemberExpression node) Visit(node.Expression); var source = _context.SqlExpression; + if (node.Member is not PropertyInfo property) + { + throw new NotSupportedException($"member {node.Member} is not supported"); + } + ISqlExpression expression = source switch { - ColumnExpression columnExpression => new ColumnsChainExpression(node.Type, new[] { columnExpression.Member, node.Member }, columnExpression.Source), - ColumnsChainExpression columnsChainExpression => new ColumnsChainExpression(node.Type, columnsChainExpression.Members.Concat(new[] { node.Member }).ToArray(), columnsChainExpression.Source), - _ => new ColumnExpression(node.Type, node.Member, source) + // TODO: test long relations chain + ColumnExpression columnExpression => new ColumnsChainExpression(node.Type, new[] { columnExpression.Member, property }, columnExpression.Source), + ColumnsChainExpression columnsChainExpression => new ColumnsChainExpression(node.Type, columnsChainExpression.Members.Concat(new[] { property }).ToArray(), columnsChainExpression.Source), + _ => new ColumnExpression(node.Type, property, source) }; _context.Remember(expression); @@ -126,21 +140,21 @@ protected override Expression VisitNew(NewExpression node) foreach (var (memberInfo, argument) in node.Members.Zip(node.Arguments, (memberInfo, argument) => (memberInfo, argument))) { + Visit(argument); + if (argument is MemberExpression memberExpression && memberExpression.Member.MemberType == MemberTypes.Property - && memberExpression.Member.Name.Equals(memberInfo.Name, StringComparison.OrdinalIgnoreCase)) + && memberExpression.Member.Name.Equals(memberInfo.Name, StringComparison.OrdinalIgnoreCase) + && _context.SqlExpression is not ColumnsChainExpression) { - Visit(argument); - var expression = _context.SqlExpression; - parameters.Add(expression); + parameters.Add(_context.SqlExpression); } else { - Visit(argument); - var expression = _context.SqlExpression is ColumnExpression + var expression = _context.SqlExpression is ColumnExpression or ColumnsChainExpression ? _context.SqlExpression : new ParenthesesExpression(_context.SqlExpression); - var renameExpression = new RenameExpression(argument.Type, memberInfo.Name, expression); + var renameExpression = new RenameExpression(argument.Type, new[] { memberInfo }, expression); parameters.Add(renameExpression); } } @@ -203,7 +217,30 @@ protected override Expression VisitUnary(UnaryExpression node) protected override Expression VisitParameter(ParameterExpression node) { - var parameterExpression = _context.ParameterExpression ?? new Expressions.ParameterExpression(node.Type, _context.NextLambdaParameterName()); + var memberExpression = _context + .Path + .Skip(1) + .FirstOrDefault() as MemberExpression; + + Type type; + + if (node.Type.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>)) + && _modelProvider + .Columns(node.Type) + .TryGetValue(memberExpression.Member.Name, out var columnInfo) + && columnInfo.IsRelation) + { + type = memberExpression.Type; + } + else + { + type = node.Type; + } + + var parameterExpression = _context + .ParameterExpressions + .SingleOrDefault(it => it.Type == type) + ?? throw new InvalidOperationException($"Unable to find parameter for type {type.FullName}"); if (ExtractUpdateQueryRootExpressionVisitor.IsUpdateQuery(_expression) || ExtractDeleteQueryRootExpressionVisitor.IsDeleteQuery(_expression)) diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs index 78a9c485..e2f6516a 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs @@ -1,7 +1,6 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation { using System; - using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using AutoRegistration.Api.Abstractions; @@ -58,22 +57,22 @@ public bool TryVisit( * select made a projection */ - var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); - - ISqlExpression predicateExpression; - - using (context.OpenParameterScope(parameterExpression)) - { - visitor.Visit(methodCallExpression.Arguments[1]); - predicateExpression = context.SqlExpression; - } - if (itemType.IsPrimitive()) { /* - * propagate primitive source projection selector + * propagate primitive from source projection selector */ + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + + ISqlExpression predicateExpression; + + using (context.OpenParametersScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + var namedSourceExpression = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); var sourceProjectionSelector = sourceProjectionExpression.Expressions.Single(); var replacedSourceProjectionSelector = ReplaceParameterSqlExpressionVisitor.Replace(sourceProjectionSelector, parameterExpression); @@ -81,27 +80,96 @@ public bool TryVisit( var replacedPredicateExpression = ReplaceParameterSqlExpressionVisitor.Replace(predicateExpression, replacedSourceProjectionSelector); var filterExpression = new FilterExpression(itemType, replacedPredicateExpression); var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); + context.Remember(projectionExpression); + } + else if (itemType.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>))) + { + /* + * propagate relation from source projection selector + */ + + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + + ISqlExpression predicateExpression; - if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + using (context.OpenParametersScope(parameterExpression)) { - projectionExpression = joinProjectionExpression; + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; } + var namedSourceExpression = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); + var expressions = sourceProjectionExpression + .Expressions + .Select(sqlExpression => ReplaceParameterSqlExpressionVisitor.Replace(sqlExpression, parameterExpression)) + .Select(sqlExpression => + { + return sqlExpression switch + { + ColumnExpression columnExpression => new ColumnExpression(columnExpression.Type, columnExpression.Member, parameterExpression), + ColumnsChainExpression columnsChainExpression => (ISqlExpression)new ColumnsChainExpression(columnsChainExpression.Type, columnsChainExpression.Members, parameterExpression), + RenameExpression renameExpression => new ColumnsChainExpression(renameExpression.Type, renameExpression.Members, parameterExpression), + _ => throw new NotSupportedException(sqlExpression.GetType().FullName) + }; + }) + .ToList(); + var filterExpression = new FilterExpression(itemType, predicateExpression); + var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); context.Remember(projectionExpression); } - else + else if (sourceProjectionExpression.Expressions.OfType().Any()) { + /* + * propagate anonymous type renames + */ + + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + + ISqlExpression predicateExpression; + + using (context.OpenParametersScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + var namedSourceExpression = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); - var expressions = RepositoryAllLinqExpressionVisitor.SelectAll(_modelProvider, itemType, parameterExpression); + var expressions = sourceProjectionExpression + .Expressions + .Select(sqlExpression => ReplaceParameterSqlExpressionVisitor.Replace(sqlExpression, parameterExpression)) + .Select(sqlExpression => + { + return sqlExpression switch + { + ColumnExpression columnExpression => new ColumnExpression(columnExpression.Type, columnExpression.Member, parameterExpression), + ColumnsChainExpression columnsChainExpression => (ISqlExpression)new ColumnsChainExpression(columnsChainExpression.Type, columnsChainExpression.Members, parameterExpression), + RenameExpression renameExpression => new ColumnsChainExpression(renameExpression.Type, renameExpression.Members, parameterExpression), + _ => throw new NotSupportedException(sqlExpression.GetType().FullName) + }; + }) + .ToList(); var filterExpression = new FilterExpression(itemType, predicateExpression); var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); + context.Remember(projectionExpression); + } + else + { + var parameterExpression = sourceProjectionExpression.Source switch + { + NamedSourceExpression namedSourceExpression => namedSourceExpression.Parameter, + _ => throw new NotSupportedException(sourceProjectionExpression.Source.GetType().FullName) + }; + + ISqlExpression predicateExpression; - if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + using (context.OpenParametersScope(parameterExpression)) { - projectionExpression = joinProjectionExpression; + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; } - context.Remember(projectionExpression); + sourceProjectionExpression.FilterExpression = new FilterExpression(itemType, predicateExpression); + context.Remember(sourceProjectionExpression); } break; @@ -117,7 +185,7 @@ public bool TryVisit( ISqlExpression predicateExpression; - using (context.OpenParameterScope(parameterExpression)) + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); predicateExpression = context.SqlExpression; @@ -135,61 +203,42 @@ public bool TryVisit( break; } - case ProjectionExpression { Source: JoinExpression joinExpression } sourceProjectionExpression: + case ProjectionExpression { Source: JoinExpression } sourceProjectionExpression: { /* * apply filter to joined source */ - if (!TryGetJoinExpressionParameter(joinExpression, itemType, out var parameterExpression)) - { - throw new InvalidOperationException("Unable to find suitable parameter expression"); - } + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); ISqlExpression predicateExpression; - using (context.OpenParameterScope(parameterExpression)) + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); predicateExpression = context.SqlExpression; } - // TODO: replace parameter + var namedSourceExpression = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); + var expressions = sourceProjectionExpression + .Expressions + .Select(sqlExpression => ReplaceParameterSqlExpressionVisitor.Replace(sqlExpression, parameterExpression)) + .Select(sqlExpression => + { + return sqlExpression switch + { + ColumnExpression columnExpression => new ColumnExpression(columnExpression.Type, columnExpression.Member, parameterExpression), + ColumnsChainExpression columnsChainExpression => (ISqlExpression)new ColumnsChainExpression(columnsChainExpression.Type, columnsChainExpression.Members, parameterExpression), + RenameExpression renameExpression => new ColumnsChainExpression(renameExpression.Type, renameExpression.Members, parameterExpression), + _ => throw new NotSupportedException(sqlExpression.GetType().FullName) + }; + }) + .ToList(); var filterExpression = new FilterExpression(itemType, predicateExpression); - sourceProjectionExpression.FilterExpression = filterExpression; - context.Remember(sourceProjectionExpression); + var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); + context.Remember(projectionExpression); break; - - static bool TryGetJoinExpressionParameter( - JoinExpression joinExpression, - Type itemType, - [NotNullWhen(true)] out Expressions.ParameterExpression? parameterExpression) - { - return TryGetJoinExpressionBranchParameter(joinExpression.LeftSource, itemType, out parameterExpression) - || TryGetJoinExpressionBranchParameter(joinExpression.RightSource, itemType, out parameterExpression); - } - - static bool TryGetJoinExpressionBranchParameter( - ISqlExpression sourceExpression, - Type itemType, - [NotNullWhen(true)] out Expressions.ParameterExpression? parameterExpression) - { - if (sourceExpression is JoinExpression leftSourceJoinExpression - && TryGetJoinExpressionParameter(leftSourceJoinExpression, itemType, out parameterExpression)) - { - return true; - } - - if (sourceExpression is NamedSourceExpression namedSourceExpression) - { - parameterExpression = namedSourceExpression.Parameter; - return true; - } - - parameterExpression = null; - return false; - } } case QuerySourceExpression querySourceExpression: @@ -199,23 +248,42 @@ static bool TryGetJoinExpressionBranchParameter( */ var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + var namedSourceExpression = new NamedSourceExpression(itemType, querySourceExpression, parameterExpression); + var expressions = SelectLinqExpressionVisitor + .SelectAll(_modelProvider, itemType, parameterExpression) + .Cast() + .ToList(); - ISqlExpression predicateExpression; + var relations = RelationsExpressionVisitor.ExtractRelations(_modelProvider, methodCallExpression.Arguments[1]); + + ProjectionExpression projectionExpression; - using (context.OpenParameterScope(parameterExpression)) + if (SelectLinqExpressionVisitor.TryBuildJoinExpression(context, _modelProvider, namedSourceExpression, relations, out var joinExpression, out var relationExpressions, out var joinParameterExpressions)) { - visitor.Visit(methodCallExpression.Arguments[1]); - predicateExpression = context.SqlExpression; - } + ISqlExpression predicateExpression; - var namedSourceExpression = new NamedSourceExpression(itemType, querySourceExpression, parameterExpression); - var expressions = RepositoryAllLinqExpressionVisitor.SelectAll(_modelProvider, itemType, parameterExpression); - var filterExpression = new FilterExpression(itemType, predicateExpression); - var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); + using (context.OpenParametersScope(joinParameterExpressions)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } - if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + expressions = expressions.Concat(relationExpressions).ToList(); + var filterExpression = new FilterExpression(itemType, predicateExpression); + projectionExpression = new ProjectionExpression(itemType, joinExpression, expressions, filterExpression, null); + } + else { - projectionExpression = joinProjectionExpression; + ISqlExpression predicateExpression; + + using (context.OpenParametersScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + + var filterExpression = new FilterExpression(itemType, predicateExpression); + projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); } context.Remember(projectionExpression); @@ -223,48 +291,50 @@ static bool TryGetJoinExpressionBranchParameter( break; } - case DeleteExpression deleteExpression: + case UpdateExpression updateExpression: { /* - * filter delete expression + * filter update expression */ var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); ISqlExpression predicateExpression; - using (context.OpenParameterScope(parameterExpression)) + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); predicateExpression = context.SqlExpression; } + // TODO: support join expression in update predicate and test it var filterExpression = new FilterExpression(itemType, predicateExpression); - deleteExpression.FilterExpression = filterExpression; - context.Remember(deleteExpression); + updateExpression.FilterExpression = filterExpression; + context.Remember(updateExpression); break; } - case UpdateExpression updateExpression: + case DeleteExpression deleteExpression: { /* - * filter update expression + * filter delete expression */ var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); ISqlExpression predicateExpression; - using (context.OpenParameterScope(parameterExpression)) + using (context.OpenParametersScope(parameterExpression)) { visitor.Visit(methodCallExpression.Arguments[1]); predicateExpression = context.SqlExpression; } + // TODO: support join expression in delete predicate and test it var filterExpression = new FilterExpression(itemType, predicateExpression); - updateExpression.FilterExpression = filterExpression; - context.Remember(updateExpression); + deleteExpression.FilterExpression = filterExpression; + context.Remember(deleteExpression); break; } @@ -318,6 +388,7 @@ internal static ISqlExpression Replace(ISqlExpression expression, ISqlExpression NullExpression nullExpression => nullExpression, Expressions.ParameterExpression => replacement, ParenthesesExpression parenthesesExpression => new ParenthesesExpression(Replace(parenthesesExpression.Source, replacement)), + RenameExpression renameExpression => new RenameExpression(renameExpression.Type, renameExpression.Members, Replace(renameExpression.Source, replacement)), QueryParameterExpression queryParameterExpression => queryParameterExpression, Expressions.UnaryExpression unaryExpression => new Expressions.UnaryExpression( unaryExpression.Type, diff --git a/tests/Tests/GenericHost.Test/LinqToSqlTests.cs b/tests/Tests/GenericHost.Test/LinqToSqlTests.cs index b2f4935a..dae0d134 100644 --- a/tests/Tests/GenericHost.Test/LinqToSqlTests.cs +++ b/tests/Tests/GenericHost.Test/LinqToSqlTests.cs @@ -403,7 +403,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => new { it.StringField, Filter = it.NullableStringField }).Where(it => it.Filter != null ? true : false)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}b.""Filter"",{Environment.NewLine}{'\t'}b.""StringField""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"" AS ""Filter""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN b.""Filter"" IS NOT NULL ELSE b.""Filter"" != @param_1 END THEN @param_2 ELSE @param_3 END", + $@"SELECT{Environment.NewLine}{'\t'}b.""StringField"",{Environment.NewLine}{'\t'}b.""Filter""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"" AS ""Filter""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN b.""Filter"" IS NOT NULL ELSE b.""Filter"" != @param_1 END THEN @param_2 ELSE @param_3 END", new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)), new SqlCommandParameter("param_3", false, typeof(bool)) }, log)), new IDatabaseEntity[] { databaseEntity } @@ -414,7 +414,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => new { it.StringField, it.NullableStringField }).Where(it => it.NullableStringField != null ? true : false)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN b.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE b.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END THEN @param_2 ELSE @param_3 END", + $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN CASE WHEN @param_0 IS NULL THEN b.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE b.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END THEN @param_2 ELSE @param_3 END", new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)), new SqlCommandParameter("param_3", false, typeof(bool)) }, log)), new IDatabaseEntity[] { databaseEntity } @@ -452,7 +452,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => new { it.BooleanField, it.StringField }).Where(it => it.BooleanField)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.BooleanField)}""", + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""", Array.Empty(), log)), new IDatabaseEntity[] { databaseEntity } @@ -474,7 +474,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => new { it.NullableStringField, it.StringField, it.IntField }).Select(it => new { it.NullableStringField, it.IntField }).Where(it => it.NullableStringField != null).Select(it => new { it.IntField }).Where(it => it.IntField > 0).Where(it => it.IntField <= 42).Select(it => it.IntField)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}f.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b) c{Environment.NewLine}{'\t'}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN c.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE c.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END) d) e{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(DatabaseEntity.IntField)}"" > @param_2 AND e.""{nameof(DatabaseEntity.IntField)}"" <= @param_3) f", + $@"SELECT{Environment.NewLine}{'\t'}f.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b) c{Environment.NewLine}{'\t'}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN c.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE c.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END) d) e{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(DatabaseEntity.IntField)}"" > @param_2 AND e.""{nameof(DatabaseEntity.IntField)}"" <= @param_3) f", new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", 0, typeof(int)), new SqlCommandParameter("param_3", 42, typeof(int)) }, log)), new IDatabaseEntity[] { databaseEntity } @@ -523,7 +523,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Where(it => it.Blog.Theme == "MilkyWay").Select(it => it.User.Nickname).Distinct()), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT DISTINCT{Environment.NewLine}{'\t'}d.""{nameof(Post.User.Nickname)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" d{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" b{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN b.""{nameof(Blog.Theme)}"" IS NULL ELSE b.""{nameof(Blog.Theme)}"" = @param_1 END) c{Environment.NewLine}ON{Environment.NewLine}{'\t'}d.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = c.""{nameof(Post.User)}""", + $@"SELECT DISTINCT{Environment.NewLine}{'\t'}e.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Version)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Version)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(AuthEndpoint.Domain.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" b{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" c{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(Post.User)}"") d{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" IS NULL ELSE d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" = @param_1 END) e", new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)) }, log)), new IDatabaseEntity[] { user, blog, post } @@ -534,7 +534,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => new { it.StringField, it.BooleanField }).Where(it => it.BooleanField).Distinct()), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT DISTINCT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.StringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.BooleanField)}""", + $@"SELECT DISTINCT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}""", Array.Empty(), log)), new IDatabaseEntity[] { databaseEntity } @@ -1015,18 +1015,40 @@ internal static IEnumerable CommandTranslationTestCases() * Relations */ + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in order by", + new Func(container => container.Resolve().All().OrderByDescending(it => it.Blog.Theme).ThenBy(it => it.User.Nickname)), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(post.Blog)}""{Environment.NewLine}ORDER BY{Environment.NewLine}{'\t'}c.""{nameof(Blog.Theme)}"" DESC, b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" ASC", + Array.Empty(), + log)), + new IDatabaseEntity[] { user, blog, post } + }; yield return new object[] { $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in filter", new Func(container => container.Resolve().All().Where(it => it.Blog.Theme == "MilkyWay" && it.User.Nickname == "SpaceEngineer")), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(Post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN c.""{nameof(Blog.Theme)}"" IS NULL ELSE c.""{nameof(Blog.Theme)}"" = @param_1 END AND CASE WHEN @param_2 IS NULL THEN b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" IS NULL ELSE b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" = @param_3 END", + $@"SELECT{Environment.NewLine}{'\t'}d.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.Version)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Version)}"",{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(AuthEndpoint.Domain.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" b{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" c{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(Post.User)}"") d{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" IS NULL ELSE d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" = @param_1 END AND CASE WHEN @param_2 IS NULL THEN d.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"" IS NULL ELSE d.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"" = @param_3 END", new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)), new SqlCommandParameter("param_2", "SpaceEngineer", typeof(string)), new SqlCommandParameter("param_3", "SpaceEngineer", typeof(string)) }, log)), new IDatabaseEntity[] { user, blog, post } }; yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in filter after projection", + new Func(container => container.Resolve().All().Select(it => new { it.Text, it.Blog, it.User }).Where(it => it.Blog.Theme == "MilkyWay")), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Post.Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Post.Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.User)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a) b{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" d{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = b.""{nameof(Post.User)}"") e{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN e.""{nameof(Blog)}_{nameof(Blog.Theme)}"" IS NULL ELSE e.""{nameof(Blog)}_{nameof(Blog.Theme)}"" = @param_1 END", + new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] { $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in projection with filter as source", new Func(container => container.Resolve().All().Where(it => it.DateTime > DateTime.MinValue).Select(it => new { it.Blog.Theme, Author = it.User.Nickname })), @@ -1049,12 +1071,45 @@ internal static IEnumerable CommandTranslationTestCases() new IDatabaseEntity[] { user, blog, post } }; yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in multi-stage projection to primitive", + new Func(container => container.Resolve().All().Select(it => new { it.Blog, it.User }).Select(it => it.Blog.Theme).Where(it => it == "MilkyWay")), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}f.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(Post.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(Post.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(Post.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.User)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a) b{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" d{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = b.""{nameof(Post.User)}"") e) f{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN f.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" IS NULL ELSE f.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" = @param_1 END", + new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in multi-stage projection to relation", + new Func(container => container.Resolve().All().Select(it => new { it.Blog, it.User }).Select(it => it.Blog).Where(it => it.Theme == "MilkyWay")), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}f.""{nameof(Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}f.""{nameof(Blog.Theme)}"",{Environment.NewLine}{'\t'}f.""{nameof(Blog.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" AS ""{nameof(Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"" AS ""{nameof(Blog.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(Post.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(Post.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(Post.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.User)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a) b{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" d{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}d.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = b.""{nameof(Post.User)}"") e) f{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN f.""{nameof(Blog.Theme)}"" IS NULL ELSE f.""{nameof(Blog.Theme)}"" = @param_1 END", + new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] + { + $"{nameof(DataAccess.Orm.Sql.Postgres)} - one-to-one relation in multi-stage projection to anonymous type", + new Func(container => container.Resolve().All().Select(it => new { it.Blog, it.User }).Select(it => new { it.Blog }).Where(it => it.Blog.Theme == "MilkyWay")), + new Action( + (query, log) => CheckSqlCommand(query, + $@"SELECT{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}b.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Post.Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Post.Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.User)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a) b{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""{Environment.NewLine}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" d{Environment.NewLine}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = b.""{nameof(Post.User)}"") e{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN e.""{nameof(Blog)}_{nameof(Blog.Theme)}"" IS NULL ELSE e.""{nameof(Blog)}_{nameof(Blog.Theme)}"" = @param_1 END", + new[] { new SqlCommandParameter("param_0", "MilkyWay", typeof(string)), new SqlCommandParameter("param_1", "MilkyWay", typeof(string)) }, + log)), + new IDatabaseEntity[] { user, blog, post } + }; + yield return new object[] { $"{nameof(DataAccess.Orm.Sql.Postgres)} - select one-to-one relation", new Func(container => container.Resolve().All().Where(it => it.DateTime > DateTime.MinValue).Select(it => it.Blog)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}c.""{nameof(Blog.Theme)}"",{Environment.NewLine}{'\t'}c.""{nameof(Blog.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"" > @param_0) b{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = b.""{nameof(Post.Blog)}""", + $@"SELECT{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"" AS ""{nameof(Blog.Theme)}"",{Environment.NewLine}{'\t'}e.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"" AS ""{nameof(Blog.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Version)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(Post.Version)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.Blog.PrimaryKey)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.Blog.Theme)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Theme)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(Post.Blog.Version)}"" AS ""{nameof(Post.Blog)}_{nameof(Post.Blog.Version)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.User.Nickname)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Nickname)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.User.PrimaryKey)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""{nameof(Post.User.Version)}"" AS ""{nameof(Post.User)}_{nameof(Post.User.Version)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}""GenericHostTest"".""Post"" a{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""GenericHostTest"".""Blog"" b{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""PrimaryKey"" = a.""Blog""{Environment.NewLine}{'\t'}{'\t'}JOIN{Environment.NewLine}{'\t'}{'\t'}{'\t'}""GenericHostTest"".""User"" c{Environment.NewLine}{'\t'}{'\t'}ON{Environment.NewLine}{'\t'}{'\t'}{'\t'}c.""PrimaryKey"" = a.""User"") d{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}d.""{nameof(Post.DateTime)}"" > @param_0) e", new[] { new SqlCommandParameter("param_0", DateTime.MinValue, typeof(DateTime)) }, log)), new IDatabaseEntity[] { user, blog, post } @@ -1064,17 +1119,6 @@ internal static IEnumerable CommandTranslationTestCases() * Order by */ - yield return new object[] - { - $"{nameof(DataAccess.Orm.Sql.Postgres)} - order by join", - new Func(container => container.Resolve().All().OrderByDescending(it => it.Blog.Theme).ThenBy(it => it.User.Nickname)), - new Action( - (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(Post.Blog)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.DateTime)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.Text)}"",{Environment.NewLine}{'\t'}a.""{nameof(Post.User)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Blog)}"" c{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntities.Relations.User)}"" b{Environment.NewLine}JOIN{Environment.NewLine}{'\t'}""{schema}"".""{nameof(Post)}"" a{Environment.NewLine}ON{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntities.Relations.User.PrimaryKey)}"" = a.""{nameof(post.User)}""{Environment.NewLine}ON{Environment.NewLine}{'\t'}c.""{nameof(Blog.PrimaryKey)}"" = a.""{nameof(post.Blog)}""{Environment.NewLine}ORDER BY{Environment.NewLine}{'\t'}c.""{nameof(Blog.Theme)}"" DESC, b.""{nameof(DatabaseEntities.Relations.User.Nickname)}"" ASC", - Array.Empty(), - log)), - new IDatabaseEntity[] { user, blog, post } - }; yield return new object[] { $"{nameof(DataAccess.Orm.Sql.Postgres)} - order by then by",