From 3c207fd61aed5800bae268e7d28b634f6331a6e2 Mon Sep 17 00:00:00 2001 From: Nikita Grishin Date: Sun, 5 May 2024 19:30:25 +0300 Subject: [PATCH] #205 --- ...xInvalidationHostedServiceStartupAction.cs | 8 + .../QuerySourceExpressionTranslator.cs | 10 +- .../UnitOfWork/IntegrationUnitOfWork.cs | 10 + ...tabaseColumnConstraintViewQueryProvider.cs | 2 +- .../Model/DatabaseColumnViewQueryProvider.cs | 2 +- .../DatabaseEnumTypeViewQueryProvider.cs | 2 +- .../DatabaseFunctionViewQueryProvider.cs | 2 +- .../Model/DatabaseIndexViewQueryProvider.cs | 2 +- .../Model/DatabaseSchemaViewQueryProvider.cs | 2 +- .../Model/DatabaseTriggerViewQueryProvider.cs | 2 +- .../Model/DatabaseViewViewQueryProvider.cs | 2 +- .../Translation/BinaryExpressionTranslator.cs | 6 +- .../Translation/ColumnExpressionTranslator.cs | 2 +- .../ColumnsChainExpressionTranslator.cs | 47 + .../Translation/DeleteExpressionTranslator.cs | 25 +- .../Translation/FilterExpressionTranslator.cs | 1 - .../Translation/InsertExpressionTranslator.cs | 2 +- .../OrderByExpressionTranslator.cs | 2 +- .../ParameterExpressionTranslator.cs | 5 + .../ProjectionExpressionTranslator.cs | 32 +- .../Translation/SetExpressionTranslator.cs | 46 - .../Translation/UpdateExpressionTranslator.cs | 36 +- .../DataAccess.Orm.Sql.csproj | 146 ++- .../Linq/SqlCommandMaterializer.cs | 101 +- .../DataAccess.Orm.Sql/Model/ColumnInfo.cs | 4 +- .../DataAccess.Orm.Sql/Model/ModelProvider.cs | 1 + .../Translation/AllLinqExpressionVisitor.cs | 67 ++ .../AnyMethodCallExpressionTranslator.cs | 57 ++ .../AsJsonObjectLinqExpressionVisitor.cs | 36 + ...AsJsonObjectUnknownExpressionTranslator.cs | 31 - ...ator.cs => AssignLinqExpressionVisitor.cs} | 24 +- .../CachedExpressionLinqExpressionVisitor.cs | 40 + .../Translation/CastLinqExpressionVisitor.cs | 36 + .../CommandParameterExtractionContext.cs | 4 +- ...ConcatJsonObjectsLinqExpressionVisitor.cs} | 24 +- .../ContainsLinqExpressionVisitor.cs | 70 ++ .../ContainsUnknownExpressionTranslator.cs | 50 - .../Translation/CountLinqExpressionVisitor.cs | 50 + ...seJsonObjectValueLinqExpressionVisitor.cs} | 10 +- .../DeleteLinqExpressionVisitor.cs | 40 + .../DistinctMerthodCallExpessionTranslator.cs | 40 + ...cs => EnumHasFlagLinqExpressionVisitor.cs} | 19 +- ...EnumerableContainsLinqExpressionVisitor.cs | 67 ++ ...ludeJsonAttributeLinqExpressionVisitor.cs} | 24 +- .../ExplainLinqExpressionVisitor.cs | 41 + .../Translation/ExpressionTranslator.cs | 21 +- .../Expressions/BinaryExpression.cs | 2 + .../Expressions/ColumnExpression.cs | 14 +- .../Expressions/ColumnsChainExpression.cs | 52 + .../Expressions/ConditionalExpression.cs | 3 + .../Expressions/DeleteExpression.cs | 17 +- .../Expressions/ExplainExpression.cs | 3 +- .../Expressions/FilterExpression.cs | 35 +- .../Expressions/InsertExpression.cs | 8 +- .../Expressions/JsonAttributeExpression.cs | 6 +- .../Expressions/MethodCallExpression.cs | 1 + .../Expressions/NamedSourceExpression.cs | 16 +- .../Translation/Expressions/NewExpression.cs | 8 +- .../Expressions/OrderByExpression.cs | 37 +- .../OrderByExpressionExpression.cs | 1 + .../Expressions/ParameterExpression.cs | 5 + .../Expressions/ParenthesesExpression.cs | 3 +- .../Expressions/ProjectionExpression.cs | 51 +- .../Expressions/RenameExpression.cs | 1 + .../Expressions/RowsFetchLimitExpression.cs | 3 +- .../Translation/Expressions/SetExpression.cs | 31 - .../Expressions/UnaryExpression.cs | 1 + .../Expressions/UpdateExpression.cs | 26 +- .../ExtractRelationsExpressionVisitor.cs | 40 - .../Translation/FirstLinqExpressionVisitor.cs | 41 + ... GetJsonAttributeLinqExpressionVisitor.cs} | 23 +- ... HasJsonAttributeLinqExpressionVisitor.cs} | 24 +- ...ranslator.cs => ILinqExpressionVisitor.cs} | 12 +- .../ISqlViewQueryProvider.cs | 3 +- .../InsertLinqExpressionVisitor.cs | 173 ++++ .../IsNotNullLinqExpressionVisitor.cs | 37 + .../IsNotNullUnknownExpressionTranslator.cs | 35 - .../IsNullLinqExpressionVisitor.cs | 37 + .../IsNullUnknownExpressionTranslator.cs | 35 - ...slator.cs => LikeLinqExpressionVisitor.cs} | 22 +- .../ObjectEqualsLinqExpressionVisitor.cs | 60 ++ ...ObjectEqualsUnknownExpressionTranslator.cs | 60 -- .../OrderByLinqExpressionVisitor.cs | 64 ++ .../RepositoryAllLinqExpressionVisitor.cs | 122 +++ .../SelectLinqExpressionVisitor.cs | 238 +++++ .../Translation/SetLinqExpressionVisitor.cs | 44 + .../SingleLinqExpressionVisitor.cs | 41 + .../Translation/SqlExpression.cs | 13 +- .../SqlExpressionTranslatorComposite.cs | 11 +- ...cs => StringEmptyLinqExpressionVisitor.cs} | 10 +- ...s => StringLengthLinqExpressionVisitor.cs} | 13 +- .../TranslationExpressionVisitor.cs | 944 +++--------------- .../UpdateLinqExpressionVisitor.cs | 41 + .../Translation/WhereLinqExpressionVisitor.cs | 331 ++++++ .../Tests/GenericHost.Test/LinqToSqlTests.cs | 10 +- 95 files changed, 2528 insertions(+), 1531 deletions(-) create mode 100644 src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql.Postgres/Translation/SetExpressionTranslator.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectLinqExpressionVisitor.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectUnknownExpressionTranslator.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{AssignUnknownExpressionTranslator.cs => AssignLinqExpressionVisitor.cs} (51%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/CachedExpressionLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/CastLinqExpressionVisitor.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{ExcludeJsonAttributeUnknownExpressionTranslator.cs => ConcatJsonObjectsLinqExpressionVisitor.cs} (52%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/ContainsLinqExpressionVisitor.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/ContainsUnknownExpressionTranslator.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{DatabaseJsonObjectValueUnknownExpressionTranslator.cs => DatabaseJsonObjectValueLinqExpressionVisitor.cs} (80%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/DeleteLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/DistinctMerthodCallExpessionTranslator.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{EnumHasFlagUnknownExpressionTranslator.cs => EnumHasFlagLinqExpressionVisitor.cs} (62%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/EnumerableContainsLinqExpressionVisitor.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{ConcatJsonObjectsUnknownExpressionTranslator.cs => ExcludeJsonAttributeLinqExpressionVisitor.cs} (52%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/ExplainLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/Expressions/SetExpression.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/ExtractRelationsExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/FirstLinqExpressionVisitor.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{GetJsonAttributeUnknownExpressionTranslator.cs => GetJsonAttributeLinqExpressionVisitor.cs} (59%) rename src/Modules/DataAccess.Orm.Sql/Translation/{HasJsonAttributeUnknownExpressionTranslator.cs => HasJsonAttributeLinqExpressionVisitor.cs} (50%) rename src/Modules/DataAccess.Orm.Sql/Translation/{IUnknownExpressionTranslator.cs => ILinqExpressionVisitor.cs} (72%) rename src/Modules/DataAccess.Orm.Sql/{Model => Translation}/ISqlViewQueryProvider.cs (92%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/InsertLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullLinqExpressionVisitor.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullUnknownExpressionTranslator.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/IsNullLinqExpressionVisitor.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/IsNullUnknownExpressionTranslator.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{LikeUnknownExpressionTranslator.cs => LikeLinqExpressionVisitor.cs} (52%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsLinqExpressionVisitor.cs delete mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsUnknownExpressionTranslator.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/RepositoryAllLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/SelectLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/SetLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/SingleLinqExpressionVisitor.cs rename src/Modules/DataAccess.Orm.Sql/Translation/{StringEmptyUnknownExpressionTranslator.cs => StringEmptyLinqExpressionVisitor.cs} (77%) rename src/Modules/DataAccess.Orm.Sql/Translation/{StringLengthUnknownExpressionTranslator.cs => StringLengthLinqExpressionVisitor.cs} (62%) create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/UpdateLinqExpressionVisitor.cs create mode 100644 src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs diff --git a/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/StartupActions/InboxInvalidationHostedServiceStartupAction.cs b/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/StartupActions/InboxInvalidationHostedServiceStartupAction.cs index 649c5147..5e5b45ee 100644 --- a/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/StartupActions/InboxInvalidationHostedServiceStartupAction.cs +++ b/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/StartupActions/InboxInvalidationHostedServiceStartupAction.cs @@ -85,6 +85,14 @@ private static async Task MarkInboxAsFailed( .ReadRequiredHeader() .Value; + /* TODO: #205 - test it + update "Deduplication"."InboxMessage" a + set "State" = 'failed'::"Deduplication"."EnInboxMessageState" + from "Deduplication"."IntegrationMessage" b + where a."Message" = b."PrimaryKey" + and b."PrimaryKey" = '502a235e-82b3-432e-9fe0-f7f2b1bb545e' + */ + await transaction .Update() .Set(inbox => inbox.State.Assign(EnInboxMessageState.Failed)) diff --git a/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/Translation/QuerySourceExpressionTranslator.cs b/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/Translation/QuerySourceExpressionTranslator.cs index c2c89ccf..0406cc0b 100644 --- a/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/Translation/QuerySourceExpressionTranslator.cs +++ b/src/Endpoints/GenericEndpoint.DataAccess.Sql.Postgres.Host/Translation/QuerySourceExpressionTranslator.cs @@ -63,13 +63,9 @@ public string Translate(QuerySourceExpression expression, int depth) } else { - sb.Append('"'); - sb.Append(_modelProvider.SchemaName(expression.ItemType)); - sb.Append('"'); - sb.Append('.'); - sb.Append('"'); - sb.Append(_modelProvider.TableName(expression.ItemType)); - sb.Append('"'); + var table = _modelProvider.Tables[expression.ItemType]; + + sb.Append($@"""{table.Schema}"".""{table.Name}"""); } return sb.ToString(); diff --git a/src/Endpoints/GenericEndpoint.DataAccess.Sql/UnitOfWork/IntegrationUnitOfWork.cs b/src/Endpoints/GenericEndpoint.DataAccess.Sql/UnitOfWork/IntegrationUnitOfWork.cs index 2776b1d7..44f91f8f 100644 --- a/src/Endpoints/GenericEndpoint.DataAccess.Sql/UnitOfWork/IntegrationUnitOfWork.cs +++ b/src/Endpoints/GenericEndpoint.DataAccess.Sql/UnitOfWork/IntegrationUnitOfWork.cs @@ -62,6 +62,7 @@ protected override async Task Commit( IAdvancedIntegrationContext context, CancellationToken token) { + // TODO: #205 - test it await (Inbox == null ? PersistInbox(context, _transaction, _endpointIdentity, EnInboxMessageState.Handled, token) : MarkInboxAsHandled(_transaction, Inbox.PrimaryKey, token)).ConfigureAwait(false); @@ -82,6 +83,15 @@ protected override async Task Rollback( CancellationToken token) { await _transaction.Close(false, token).ConfigureAwait(false); + + // TODO: #205 - test it + if (Inbox == null) + { + await using (await _transaction.OpenScope(true, token).ConfigureAwait(false)) + { + await PersistInbox(context, _transaction, _endpointIdentity, EnInboxMessageState.Processing, token).ConfigureAwait(false); + } + } } private static Task ReadInbox( diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnConstraintViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnConstraintViewQueryProvider.cs index 846b2c93..360f6875 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnConstraintViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnConstraintViewQueryProvider.cs @@ -5,7 +5,7 @@ using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseColumnConstraintViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnViewQueryProvider.cs index ccd1fa75..0b763326 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseColumnViewQueryProvider.cs @@ -5,7 +5,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Postgres.Model using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseColumnViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseEnumTypeViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseEnumTypeViewQueryProvider.cs index ac7e24b9..666cced0 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseEnumTypeViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseEnumTypeViewQueryProvider.cs @@ -5,7 +5,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Postgres.Model using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseEnumTypeViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseFunctionViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseFunctionViewQueryProvider.cs index df75e840..95728349 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseFunctionViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseFunctionViewQueryProvider.cs @@ -5,7 +5,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Postgres.Model using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseFunctionViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseIndexViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseIndexViewQueryProvider.cs index 2b121c06..bf84cd65 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseIndexViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseIndexViewQueryProvider.cs @@ -5,7 +5,7 @@ using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseIndexViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseSchemaViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseSchemaViewQueryProvider.cs index 7844cbd3..b53483d6 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseSchemaViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseSchemaViewQueryProvider.cs @@ -5,7 +5,7 @@ using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseSchemaViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseTriggerViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseTriggerViewQueryProvider.cs index bfb44a42..ef87ca04 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseTriggerViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseTriggerViewQueryProvider.cs @@ -5,7 +5,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Postgres.Model using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseTriggerViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseViewViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseViewViewQueryProvider.cs index f788135c..88c312e6 100644 --- a/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseViewViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql.Migrations.Portgres/Model/DatabaseViewViewQueryProvider.cs @@ -5,7 +5,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Postgres.Model using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using SpaceEngineers.Core.DataAccess.Orm.Sql.Migrations.Model; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; + using Translation; [Component(EnLifestyle.Singleton)] internal class DatabaseViewViewQueryProvider : ISqlViewQueryProvider, diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/BinaryExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/BinaryExpressionTranslator.cs index 919cff2a..8423df4d 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/BinaryExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/BinaryExpressionTranslator.cs @@ -69,13 +69,11 @@ public string Translate(BinaryExpression expression, int depth) { sb.Append(_translator.Translate(expression.Left, depth)); sb.Append(" = ANY"); - sb.Append('('); sb.Append(_translator.Translate(expression.Right, depth + 1)); - sb.Append(')'); } - else if (FunctionalOperators.ContainsKey(expression.Operator)) + else if (FunctionalOperators.TryGetValue(expression.Operator, out var @operator)) { - sb.Append(FunctionalOperators[expression.Operator]); + sb.Append(@operator); sb.Append('('); sb.Append(_translator.Translate(expression.Left, depth)); sb.Append(", "); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs index 21c1200e..968c7bcf 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnExpressionTranslator.cs @@ -31,7 +31,7 @@ public string Translate(ColumnExpression expression, int depth) { var sb = new StringBuilder(); - if (expression.Source != null) + if (expression.Source is not ParameterExpression { SkipInSql: true }) { sb.Append(_translator.Translate(expression.Source, depth)); sb.Append('.'); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs new file mode 100644 index 00000000..b0fe8106 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ColumnsChainExpressionTranslator.cs @@ -0,0 +1,47 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Postgres.Translation +{ + using System; + using System.Text; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using SpaceEngineers.Core.DataAccess.Orm.Sql.Translation; + using SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions; + + [Component(EnLifestyle.Singleton)] + internal class ColumnsChainExpressionTranslator : ISqlExpressionTranslator, + IResolvable>, + ICollectionResolvable + { + private readonly ISqlExpressionTranslatorComposite _translator; + + public ColumnsChainExpressionTranslator(ISqlExpressionTranslatorComposite translator) + { + _translator = translator; + } + + public string Translate(ISqlExpression expression, int depth) + { + return expression is ColumnsChainExpression columnsChainExpression + ? Translate(columnsChainExpression, depth) + : throw new NotSupportedException($"Unsupported sql expression type {expression.GetType()}"); + } + + public string Translate(ColumnsChainExpression expression, int depth) + { + var sb = new StringBuilder(); + + if (expression.Source is not ParameterExpression { SkipInSql: true }) + { + sb.Append(_translator.Translate(expression.Source, depth)); + sb.Append('.'); + } + + sb.Append('"'); + sb.Append(expression.Name); + sb.Append('"'); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/DeleteExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/DeleteExpressionTranslator.cs index a5d5ffc5..4f9504c6 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/DeleteExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/DeleteExpressionTranslator.cs @@ -1,6 +1,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Postgres.Translation { using System; + using System.Text; using AutoRegistration.Api.Abstractions; using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; @@ -14,10 +15,14 @@ internal class DeleteExpressionTranslator : ISqlExpressionTranslator { private readonly IModelProvider _modelProvider; + private readonly ISqlExpressionTranslatorComposite _translator; - public DeleteExpressionTranslator(IModelProvider modelProvider) + public DeleteExpressionTranslator( + IModelProvider modelProvider, + ISqlExpressionTranslatorComposite translator) { _modelProvider = modelProvider; + _translator = translator; } public string Translate(ISqlExpression expression, int depth) @@ -29,9 +34,23 @@ public string Translate(ISqlExpression expression, int depth) public string Translate(DeleteExpression expression, int depth) { - var table = _modelProvider.Tables[expression.Type]; + var sb = new StringBuilder(); - return $@"DELETE FROM ""{table.Schema}"".""{table.Name}"""; + var table = _modelProvider.Tables[expression.ItemType]; + + sb.Append(new string('\t', depth)); + + if (expression.FilterExpression != null) + { + sb.AppendLine($@"DELETE FROM ""{table.Schema}"".""{table.Name}"""); + sb.Append(_translator.Translate(expression.FilterExpression, depth)); + } + else + { + sb.Append($@"DELETE FROM ""{table.Schema}"".""{table.Name}"""); + } + + return sb.ToString(); } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/FilterExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/FilterExpressionTranslator.cs index fd557450..6ff6b9e1 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/FilterExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/FilterExpressionTranslator.cs @@ -31,7 +31,6 @@ public string Translate(FilterExpression expression, int depth) { var sb = new StringBuilder(); - sb.AppendLine(_translator.Translate(expression.Source, depth)); sb.Append(new string('\t', depth)); sb.AppendLine("WHERE"); sb.Append(new string('\t', depth + 1)); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/InsertExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/InsertExpressionTranslator.cs index f98363bd..cb61e90a 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/InsertExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/InsertExpressionTranslator.cs @@ -46,7 +46,7 @@ public string Translate(InsertExpression expression, int depth) { var sb = new StringBuilder(); - var table = _modelProvider.Tables[expression.Type]; + var table = _modelProvider.Tables[expression.ItemType]; sb.Append(CultureInfo.InvariantCulture, $@"INSERT INTO ""{table.Schema}"".""{table.Name}"" "); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/OrderByExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/OrderByExpressionTranslator.cs index 453b99fc..c5b510ec 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/OrderByExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/OrderByExpressionTranslator.cs @@ -33,7 +33,7 @@ public string Translate(OrderByExpression expression, int depth) { var sb = new StringBuilder(); - sb.AppendLine(_translator.Translate(expression.Source, depth)); + sb.Append(new string('\t', depth)); sb.AppendLine("ORDER BY"); sb.Append(new string('\t', depth + 1)); diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ParameterExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ParameterExpressionTranslator.cs index 991a176a..e46284a4 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ParameterExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ParameterExpressionTranslator.cs @@ -21,6 +21,11 @@ public string Translate(ISqlExpression expression, int depth) public string Translate(ParameterExpression expression, int depth) { + if (expression.SkipInSql) + { + return string.Empty; + } + return expression.Name; } } diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ProjectionExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ProjectionExpressionTranslator.cs index 31656777..61f27058 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ProjectionExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/ProjectionExpressionTranslator.cs @@ -46,7 +46,37 @@ public string Translate(ProjectionExpression expression, int depth) sb.Append(new string('\t', depth)); sb.AppendLine("FROM"); sb.Append(new string('\t', depth + 1)); - sb.Append(_translator.Translate(expression.Source, depth + 1)); + + var sourceExpression = _translator.Translate(expression.Source, depth + 1); + + if (expression.FilterExpression != null + || expression.OrderByExpression != null) + { + sb.AppendLine(sourceExpression); + } + else + { + sb.Append(sourceExpression); + } + + if (expression.FilterExpression != null) + { + var filterExpression = _translator.Translate(expression.FilterExpression, depth); + + if (expression.OrderByExpression != null) + { + sb.AppendLine(filterExpression); + } + else + { + sb.Append(filterExpression); + } + } + + if (expression.OrderByExpression != null) + { + sb.Append(_translator.Translate(expression.OrderByExpression, depth)); + } return sb.ToString(); } diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/SetExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/SetExpressionTranslator.cs deleted file mode 100644 index 24817966..00000000 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/SetExpressionTranslator.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Postgres.Translation -{ - using System; - using System.Linq; - using System.Text; - using AutoRegistration.Api.Abstractions; - using AutoRegistration.Api.Attributes; - using AutoRegistration.Api.Enumerations; - using Basics; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Translation; - using SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions; - - [Component(EnLifestyle.Singleton)] - internal class SetExpressionTranslator : ISqlExpressionTranslator, - IResolvable>, - ICollectionResolvable - { - private readonly ISqlExpressionTranslatorComposite _translator; - - public SetExpressionTranslator(ISqlExpressionTranslatorComposite translator) - { - _translator = translator; - } - - public string Translate(ISqlExpression expression, int depth) - { - return expression is SetExpression setExpression - ? Translate(setExpression, depth) - : throw new NotSupportedException($"Unsupported sql expression type {expression.GetType()}"); - } - - public string Translate(SetExpression expression, int depth) - { - var sb = new StringBuilder(); - - sb.AppendLine(_translator.Translate(expression.Source, depth)); - sb.Append("SET "); - sb.Append(expression - .Assignments - .Select(assignment => _translator.Translate(assignment, depth)) - .ToString(", ")); - - return sb.ToString(); - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/UpdateExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/UpdateExpressionTranslator.cs index 48801ed6..fceb0331 100644 --- a/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/UpdateExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql.Postgres/Translation/UpdateExpressionTranslator.cs @@ -1,9 +1,12 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Postgres.Translation { using System; + using System.Linq; + using System.Text; using AutoRegistration.Api.Abstractions; using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; + using Basics; using SpaceEngineers.Core.DataAccess.Orm.Sql.Model; using SpaceEngineers.Core.DataAccess.Orm.Sql.Translation; using SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions; @@ -14,10 +17,14 @@ internal class UpdateExpressionTranslator : ISqlExpressionTranslator { private readonly IModelProvider _modelProvider; + private readonly ISqlExpressionTranslatorComposite _translator; - public UpdateExpressionTranslator(IModelProvider modelProvider) + public UpdateExpressionTranslator( + IModelProvider modelProvider, + ISqlExpressionTranslatorComposite translator) { _modelProvider = modelProvider; + _translator = translator; } public string Translate(ISqlExpression expression, int depth) @@ -29,9 +36,32 @@ public string Translate(ISqlExpression expression, int depth) public string Translate(UpdateExpression expression, int depth) { - var table = _modelProvider.Tables[expression.Type]; + var sb = new StringBuilder(); - return $@"UPDATE ""{table.Schema}"".""{table.Name}"""; + var table = _modelProvider.Tables[expression.ItemType]; + + sb.Append(new string('\t', depth)); + sb.AppendLine($@"UPDATE ""{table.Schema}"".""{table.Name}"""); + + sb.Append(new string('\t', depth)); + sb.Append("SET "); + + var assignments = expression + .Assignments + .Select(assignment => new string('\t', depth) + _translator.Translate(assignment, depth)) + .ToString(", "); + + if (expression.FilterExpression != null) + { + sb.AppendLine(assignments); + sb.Append(_translator.Translate(expression.FilterExpression, depth)); + } + else + { + sb.Append(assignments); + } + + return sb.ToString(); } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj b/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj index 148788cf..c8bfe0c4 100644 --- a/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj +++ b/src/Modules/DataAccess.Orm.Sql/DataAccess.Orm.Sql.csproj @@ -24,26 +24,20 @@ ISqlView.cs - - ExpressionTranslator.cs - - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - ExpressionTranslator.cs - - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs BinaryExpression.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs ISqlExpression.cs @@ -99,14 +93,11 @@ ISqlExpression.cs - - TranslationExpressionVisitor.cs - ISqlExpression.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs ColumnInfo.cs @@ -153,11 +144,11 @@ SqlExpression.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs SqlQuery.cs @@ -171,11 +162,8 @@ TranslationExpressionVisitor.cs - - IUnknownExpressionTranslator.cs - - - ISqlExpression.cs + + ILinqExpressionVisitor.cs ISqlExpression.cs @@ -184,7 +172,7 @@ ISqlExpression.cs - ISqlExpression.cs + InsertExpression.cs LinqExtensions.cs @@ -309,9 +297,6 @@ IUniqueIdentified.cs - - BaseSqlView.cs - ColumnInfo.cs @@ -324,29 +309,29 @@ ISqlExpression.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs ISqlExpression.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs - - IUnknownExpressionTranslator.cs + + ILinqExpressionVisitor.cs ISqlExpression.cs @@ -360,5 +345,74 @@ ISqlExpression.cs + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + IMethodCallExpressionTranslator.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ISqlViewQueryProvider.cs + + + ILinqExpressionVisitor.cs + + + ILinqExpressionVisitor.cs + + + ExpressionTranslator.cs + + + ExpressionTranslator.cs + + + ISqlExpression.cs + diff --git a/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs b/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs index 754d9a70..1e74ce8e 100644 --- a/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs +++ b/src/Modules/DataAccess.Orm.Sql/Linq/SqlCommandMaterializer.cs @@ -91,25 +91,25 @@ private async IAsyncEnumerable MaterializeInternal( var multipleRelationValues = InitializeMultipleRelations(type, values); + var arrangedValues = ArrangeValues(type, values); + object? built; if (type.IsDatabaseEntity() - && TryGetValue(transaction, type, values[nameof(IUniqueIdentified.PrimaryKey)] !, out var stored)) + && TryGetValueFromTransaction(transaction, type, values[nameof(IUniqueIdentified.PrimaryKey)] !, out var stored)) { - _objectBuilder.Fill(type, stored, ArrangeValues(type, values)); + // TODO: why refill from query results? + _objectBuilder.Fill(type, stored, arrangedValues); built = stored; } else { - built = _objectBuilder.Build(type, ArrangeValues(type, values)) !; + built = _objectBuilder.Build(type, arrangedValues) !; } - if (built is IUniqueIdentified uniqueIdentified) - { - Store(transaction, uniqueIdentified); - } + StoreInTransaction(transaction, built); - await MaterializeRelations(transaction, built, relationValues, token).ConfigureAwait(false); + MaterializeRelations(transaction, built, relationValues); await MaterializeMultipleRelations(transaction, built, type, multipleRelationValues, token).ConfigureAwait(false); @@ -245,14 +245,14 @@ static bool TryDeserializeJson( } } - private bool TryGetValue( + private bool TryGetValueFromTransaction( IAdvancedDatabaseTransaction transaction, Type entity, object key, [NotNullWhen(true)] out object? stored) { stored = GetType() - .CallMethod(nameof(TryGetValue)) + .CallMethod(nameof(TryGetValueFromTransaction)) .WithTypeArguments(entity) .WithArguments(transaction.Store, key) .Invoke(); @@ -260,7 +260,7 @@ private bool TryGetValue( return stored != default; } - private static TEntity? TryGetValue( + private static TEntity? TryGetValueFromTransaction( ITransactionalStore transactionalStore, object key) where TEntity : IUniqueIdentified @@ -270,14 +270,17 @@ private bool TryGetValue( : default; } - private static void Store( + private static void StoreInTransaction( IAdvancedDatabaseTransaction transaction, - IUniqueIdentified built) + object built) { - transaction.Store.Store(built); + if (built is IUniqueIdentified uniqueIdentified) + { + transaction.Store.Store(uniqueIdentified); + } } - private IReadOnlyDictionary InitializeRelations( + private IReadOnlyDictionary Values)> InitializeRelations( Type type, IDictionary values) { @@ -289,22 +292,43 @@ private static void Store( column => column, column => { - var value = values[column.Name]; + /* + * initialize database entity with null relation and set it later + */ + + var primaryKey = values[column.Name]; values[column.Name] = column.Relation.Target.DefaultValue(); - return value; + var relationKeys = values + .Select(pair => pair.Key) + .Where(key => key.StartsWith($"{column.Name}_")) + .ToList(); + + var relationValues = new Dictionary(); + + foreach (var relationKey in relationKeys) + { + if (values.Remove(relationKey, out var value)) + { + var cleanRelationKey = relationKey.Substring(column.Name.Length + 1); + relationValues[cleanRelationKey] = value; + } + } + + return (primaryKey, (IDictionary)relationValues); }); } - private static async Task MaterializeRelations( + private void MaterializeRelations( IAdvancedDatabaseTransaction transaction, object? built, - IReadOnlyDictionary relationValues, - CancellationToken token) + IReadOnlyDictionary)> relationValues) { - foreach (var (column, primaryKey) in relationValues) + foreach (var (column, pair) in relationValues) { + var (primaryKey, arrangedValues) = pair; + if (primaryKey is null or DBNull) { continue; @@ -312,43 +336,28 @@ private static async Task MaterializeRelations( var relation = column.Table.IsMtmTable ? primaryKey - : await MaterializeRelation(transaction, column.Relation.Target, primaryKey, token).ConfigureAwait(false); + : MaterializeRelation(transaction, column.Relation.Target, primaryKey, arrangedValues); column.Relation.Property.Declared.SetValue(built, relation); } } - private static async Task MaterializeRelation( + private object MaterializeRelation( IAdvancedDatabaseTransaction transaction, Type type, object primaryKey, - CancellationToken token) - { - var keyType = type.ExtractGenericArgumentAt(typeof(IUniqueIdentified<>)); - - return await typeof(SqlCommandMaterializer) - .CallMethod(nameof(MaterializeRelation)) - .WithTypeArguments(type, keyType) - .WithArguments(transaction, primaryKey, token) - .Invoke>() - .ConfigureAwait(false); - } - - private static async Task MaterializeRelation( - IAdvancedDatabaseTransaction transaction, - TKey primaryKey, - CancellationToken token) - where TEntity : IDatabaseEntity - where TKey : notnull + IDictionary values) { - if (transaction.Store.TryGetValue(primaryKey, out var entity)) + if (TryGetValueFromTransaction(transaction, type, primaryKey, out var relation)) { - return entity; + return relation; } - return await transaction - .SingleOrDefault(primaryKey, token) - .ConfigureAwait(false); + relation = _objectBuilder.Build(type, values) !; + + StoreInTransaction(transaction, relation); + + return relation; } private IReadOnlyDictionary InitializeMultipleRelations( diff --git a/src/Modules/DataAccess.Orm.Sql/Model/ColumnInfo.cs b/src/Modules/DataAccess.Orm.Sql/Model/ColumnInfo.cs index 9f83774f..0fa9bb24 100644 --- a/src/Modules/DataAccess.Orm.Sql/Model/ColumnInfo.cs +++ b/src/Modules/DataAccess.Orm.Sql/Model/ColumnInfo.cs @@ -289,7 +289,7 @@ public ColumnExpression BuildExpression(ParameterExpression parameter) if (Relation != null) { - return new ColumnExpression(Relation.Property.Reflected, Type, parameter); + return new ColumnExpression(Type, Relation.Property.Reflected, parameter); } _sqlExpressionExtractor ??= GetValueExtractor(_chain, AggregateColumns); @@ -298,7 +298,7 @@ public ColumnExpression BuildExpression(ParameterExpression parameter) static ISqlExpression AggregateColumns(PropertyInfo property, ISqlExpression source) { - return new ColumnExpression(property, property.PropertyType, source); + return new ColumnExpression(property.PropertyType, property, source); } } diff --git a/src/Modules/DataAccess.Orm.Sql/Model/ModelProvider.cs b/src/Modules/DataAccess.Orm.Sql/Model/ModelProvider.cs index 73543a71..398d1977 100644 --- a/src/Modules/DataAccess.Orm.Sql/Model/ModelProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql/Model/ModelProvider.cs @@ -12,6 +12,7 @@ using Basics; using Dynamic; using Dynamic.Abstractions; + using Translation; [Component(EnLifestyle.Singleton)] internal class ModelProvider : IModelProvider, diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs new file mode 100644 index 00000000..b7f37e68 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/AllLinqExpressionVisitor.cs @@ -0,0 +1,67 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class AllLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableAll()) + { + /* + * (count(case when then 1 else null end) = count(*)) as "All" + */ + + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + visitor.Visit(methodCallExpression.Arguments[0]); + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + var source = new NamedSourceExpression(itemType, context.SqlExpression, parameterExpression); + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + } + + var when = context.SqlExpression; + var name = context.NextCommandParameterName(); + var extractor = new Func(static _ => Expression.Constant(1, typeof(int))); + context.CaptureCommandParameterExtractor(name, extractor); + var then = new QueryParameterExpression(typeof(int), name); + var @else = new NullExpression(); + var conditionalExpression = new Expressions.ConditionalExpression(typeof(int), when, then, @else); + var left = new Expressions.MethodCallExpression(typeof(int), nameof(Queryable.Count), null, new[] { conditionalExpression }); + 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 projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }, null, null); + context.Remember(projectionExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs new file mode 100644 index 00000000..16b1fbe6 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/AnyMethodCallExpressionTranslator.cs @@ -0,0 +1,57 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class SelectAnyLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableAny()) + { + /* + * count(*) > 0 as "Any" + */ + + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + visitor.Visit(methodCallExpression.Arguments[0]); + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + ISqlExpression source = new NamedSourceExpression(itemType, new ParenthesesExpression(context.SqlExpression), parameterExpression); + var left = new Expressions.MethodCallExpression(typeof(int), nameof(Queryable.Count), null, new[] { new StarExpression() }); + var name = context.NextCommandParameterName(); + var extractor = new Func(static _ => Expression.Constant(0, typeof(int))); + context.CaptureCommandParameterExtractor(name, extractor); + 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 projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }, null, null); + context.Remember(projectionExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectLinqExpressionVisitor.cs new file mode 100644 index 00000000..c1494c00 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectLinqExpressionVisitor.cs @@ -0,0 +1,36 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class AsJsonObjectLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.AsJsonObject()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectUnknownExpressionTranslator.cs deleted file mode 100644 index 69fd1ae7..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/AsJsonObjectUnknownExpressionTranslator.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation -{ - using System.Linq.Expressions; - using AutoRegistration.Api.Abstractions; - using AutoRegistration.Api.Attributes; - using AutoRegistration.Api.Enumerations; - using Basics; - using Linq; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; - - [Component(EnLifestyle.Singleton)] - internal class AsJsonObjectUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable - { - public bool TryTranslate( - TranslationContext context, - Expression expression, - ExpressionVisitor visitor) - { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.AsJsonObject()) - { - visitor.Visit(methodCallExpression.Arguments[0]); - - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/AssignUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/AssignLinqExpressionVisitor.cs similarity index 51% rename from src/Modules/DataAccess.Orm.Sql/Translation/AssignUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/AssignLinqExpressionVisitor.cs index 2efc313c..9e61323e 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/AssignUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/AssignLinqExpressionVisitor.cs @@ -7,26 +7,30 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Basics; using Expressions; using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class AssignUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class AssignLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.Assign()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.Assign()) { visitor.Visit(methodCallExpression.Arguments[0]); var left = context.SqlExpression; visitor.Visit(methodCallExpression.Arguments[1]); var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(void), BinaryOperator.Assign, left, right); + var binaryExpression = new Expressions.BinaryExpression(typeof(void), BinaryOperator.Assign, left, right); context.Remember(binaryExpression); return true; diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/CachedExpressionLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/CachedExpressionLinqExpressionVisitor.cs new file mode 100644 index 00000000..35769a70 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/CachedExpressionLinqExpressionVisitor.cs @@ -0,0 +1,40 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class CachedExpressionLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.CachedExpression() + || method == LinqMethods.CachedInsertExpression() + || method == LinqMethods.CachedUpdateExpression() + || method == LinqMethods.CachedDeleteExpression() + || method == LinqMethods.WithDependencyContainer()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/CastLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/CastLinqExpressionVisitor.cs new file mode 100644 index 00000000..a070e615 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/CastLinqExpressionVisitor.cs @@ -0,0 +1,36 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class CastLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableCast()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/CommandParameterExtractionContext.cs b/src/Modules/DataAccess.Orm.Sql/Translation/CommandParameterExtractionContext.cs index a2fdeb80..9c8aeb50 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/CommandParameterExtractionContext.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/CommandParameterExtractionContext.cs @@ -176,13 +176,13 @@ private static bool TryFold( case ConstantExpression constantExpression: { if (next is MethodCallExpression methodCallExpression - && methodCallExpression.Method == TranslationExpressionVisitor.GetInsertValuesMethod + && methodCallExpression.Method == InsertLinqExpressionVisitor.GetInsertValuesMethod && methodCallExpression.Arguments[0] is ConstantExpression firstArgument && firstArgument.Value is IModelProvider modelProvider) { extractor = context => { - var insertValuesMap = (IReadOnlyDictionary)TranslationExpressionVisitor.GetInsertValuesMethod.Invoke( + var insertValuesMap = (IReadOnlyDictionary)InsertLinqExpressionVisitor.GetInsertValuesMethod.Invoke( null, new[] { diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ExcludeJsonAttributeUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ConcatJsonObjectsLinqExpressionVisitor.cs similarity index 52% rename from src/Modules/DataAccess.Orm.Sql/Translation/ExcludeJsonAttributeUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/ConcatJsonObjectsLinqExpressionVisitor.cs index 1d403333..716944aa 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/ExcludeJsonAttributeUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ConcatJsonObjectsLinqExpressionVisitor.cs @@ -7,26 +7,30 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Basics; using Expressions; using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class ExcludeJsonAttributeUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class ConcatJsonObjectsLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.ExcludeJsonAttribute()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.ConcatJsonObjects()) { visitor.Visit(methodCallExpression.Arguments[0]); var left = context.SqlExpression; visitor.Visit(methodCallExpression.Arguments[1]); var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(void), BinaryOperator.Subtract, left, right); + var binaryExpression = new Expressions.BinaryExpression(typeof(void), BinaryOperator.ConcatJsonObjects, left, right); var parenthesesExpression = new ParenthesesExpression(binaryExpression); context.Remember(parenthesesExpression); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ContainsLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ContainsLinqExpressionVisitor.cs new file mode 100644 index 00000000..cd69f00f --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ContainsLinqExpressionVisitor.cs @@ -0,0 +1,70 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class ContainsLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + private readonly TranslationExpressionVisitor _translationExpressionVisitor; + + public ContainsLinqExpressionVisitor(TranslationExpressionVisitor translationExpressionVisitor) + { + _translationExpressionVisitor = translationExpressionVisitor; + } + + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableContains()) + { + if (methodCallExpression.Arguments[0] is not ConstantExpression constantExpression + || constantExpression.Value is not IQueryable subQuery) + { + throw new InvalidOperationException("Unable to translate sub-query"); + } + + visitor.Visit(methodCallExpression.Arguments[1]); + var left = context.SqlExpression; + ISqlExpression right; + + using (context.WithinPathScope(constantExpression)) + { + var subQueryExpression = TranslateSubQuery(context, subQuery.Expression).Expression; + right = new ParenthesesExpression(subQueryExpression); + } + + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Contains, left, right); + context.Remember(binaryExpression); + + return true; + } + + return false; + } + + private SqlExpression TranslateSubQuery( + TranslationContext context, + Expression expression) + { + return _translationExpressionVisitor.Translate(context.Clone(), expression); + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ContainsUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ContainsUnknownExpressionTranslator.cs deleted file mode 100644 index 37708e7d..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/ContainsUnknownExpressionTranslator.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq.Expressions; - using System.Reflection; - using AutoRegistration.Api.Abstractions; - using AutoRegistration.Api.Attributes; - using AutoRegistration.Api.Enumerations; - using Basics; - using Expressions; - using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; - - [Component(EnLifestyle.Singleton)] - internal class ContainsUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable - { - public bool TryTranslate( - TranslationContext context, - Expression expression, - ExpressionVisitor visitor) - { - if (expression is MethodCallExpression methodCallExpression - && (methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.EnumerableContains() - || IsCollectionContains(methodCallExpression.Method))) - { - visitor.Visit(methodCallExpression.Arguments[1]); - var left = context.SqlExpression; - visitor.Visit(methodCallExpression.Arguments[0]); - var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(bool), BinaryOperator.Contains, left, right); - context.Remember(binaryExpression); - - return true; - } - - return false; - - static bool IsCollectionContains(MethodInfo methodInfo) - { - return typeof(ICollection).IsAssignableFrom(methodInfo.DeclaringType) - && methodInfo.Name.Equals(nameof(ICollection.Contains), StringComparison.OrdinalIgnoreCase) - && methodInfo.GetParameters().Length == 1; - } - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs new file mode 100644 index 00000000..9feee935 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/CountLinqExpressionVisitor.cs @@ -0,0 +1,50 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class CountLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableCount()) + { + /* + * count(*) as "Count" + */ + + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + visitor.Visit(methodCallExpression.Arguments[0]); + 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 projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }, null, null); + context.Remember(projectionExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/DatabaseJsonObjectValueUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/DatabaseJsonObjectValueLinqExpressionVisitor.cs similarity index 80% rename from src/Modules/DataAccess.Orm.Sql/Translation/DatabaseJsonObjectValueUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/DatabaseJsonObjectValueLinqExpressionVisitor.cs index d3ede3f8..e11ca949 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/DatabaseJsonObjectValueUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/DatabaseJsonObjectValueLinqExpressionVisitor.cs @@ -9,13 +9,13 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Linq; [Component(EnLifestyle.Singleton)] - internal class DatabaseJsonObjectValueUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class DatabaseJsonObjectValueLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { if (expression is MemberExpression memberExpression && memberExpression.Member is PropertyInfo propertyInfo diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/DeleteLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/DeleteLinqExpressionVisitor.cs new file mode 100644 index 00000000..09a6d888 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/DeleteLinqExpressionVisitor.cs @@ -0,0 +1,40 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class DeleteLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.RepositoryDelete()) + { + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + var deleteExpression = new DeleteExpression(itemType, null); + context.Remember(deleteExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/DistinctMerthodCallExpessionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/DistinctMerthodCallExpessionTranslator.cs new file mode 100644 index 00000000..37135bb7 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/DistinctMerthodCallExpessionTranslator.cs @@ -0,0 +1,40 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class DistinctLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableDistinct()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var projection = (ProjectionExpression)context.SqlExpression; + projection.IsDistinct = true; + context.Remember(projection); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/EnumHasFlagUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/EnumHasFlagLinqExpressionVisitor.cs similarity index 62% rename from src/Modules/DataAccess.Orm.Sql/Translation/EnumHasFlagUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/EnumHasFlagLinqExpressionVisitor.cs index b4e7d130..450568f1 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/EnumHasFlagUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/EnumHasFlagLinqExpressionVisitor.cs @@ -6,19 +6,22 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using AutoRegistration.Api.Enumerations; using Expressions; using Linq; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class EnumHasFlagUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class EnumHasFlagLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method == LinqMethods.EnumHasFlag()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + if (methodCallExpression.Method == LinqMethods.EnumHasFlag()) { visitor.Visit(methodCallExpression.Object); var left = context.SqlExpression; diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/EnumerableContainsLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/EnumerableContainsLinqExpressionVisitor.cs new file mode 100644 index 00000000..b637439e --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/EnumerableContainsLinqExpressionVisitor.cs @@ -0,0 +1,67 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq.Expressions; + using System.Reflection; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class EnumerableContainsLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.EnumerableContains()) + { + visitor.Visit(methodCallExpression.Arguments[1]); + var left = context.SqlExpression; + visitor.Visit(methodCallExpression.Arguments[0]); + var right = new ParenthesesExpression(context.SqlExpression); + + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Contains, left, right); + context.Remember(binaryExpression); + + return true; + } + + if (IsCollectionContains(method)) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var left = context.SqlExpression; + visitor.Visit(methodCallExpression.Object); + var right = new ParenthesesExpression(context.SqlExpression); + + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Contains, left, right); + context.Remember(binaryExpression); + + return true; + } + + return false; + } + + private static bool IsCollectionContains(MethodInfo methodInfo) + { + return typeof(ICollection).IsAssignableFrom(methodInfo.DeclaringType) + && methodInfo.Name.Equals(nameof(ICollection.Contains), StringComparison.OrdinalIgnoreCase) + && methodInfo.GetParameters().Length == 1; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ConcatJsonObjectsUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ExcludeJsonAttributeLinqExpressionVisitor.cs similarity index 52% rename from src/Modules/DataAccess.Orm.Sql/Translation/ConcatJsonObjectsUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/ExcludeJsonAttributeLinqExpressionVisitor.cs index faf105f2..a73a1f8f 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/ConcatJsonObjectsUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ExcludeJsonAttributeLinqExpressionVisitor.cs @@ -7,26 +7,30 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Basics; using Expressions; using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class ConcatJsonObjectsUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class ExcludeJsonAttributeLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.ConcatJsonObjects()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.ExcludeJsonAttribute()) { visitor.Visit(methodCallExpression.Arguments[0]); var left = context.SqlExpression; visitor.Visit(methodCallExpression.Arguments[1]); var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(void), BinaryOperator.ConcatJsonObjects, left, right); + var binaryExpression = new Expressions.BinaryExpression(typeof(void), BinaryOperator.Subtract, left, right); var parenthesesExpression = new ParenthesesExpression(binaryExpression); context.Remember(parenthesesExpression); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ExplainLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ExplainLinqExpressionVisitor.cs new file mode 100644 index 00000000..da57dc64 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ExplainLinqExpressionVisitor.cs @@ -0,0 +1,41 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class ExplainLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.Explain()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var source = context.SqlExpression; + var analyze = (bool)((ConstantExpression)methodCallExpression.Arguments[1]).Value; + var explainExpression = new ExplainExpression(source, analyze); + context.Remember(explainExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ExpressionTranslator.cs index f5792dbb..2a40078e 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/ExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ExpressionTranslator.cs @@ -2,7 +2,6 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation { using System; using System.Collections.Concurrent; - using System.Collections.Generic; using System.Linq.Expressions; using AutoRegistration.Api.Abstractions; using AutoRegistration.Api.Attributes; @@ -12,32 +11,25 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Exceptions; using Linq; using Microsoft.Extensions.Logging; - using Model; [Component(EnLifestyle.Singleton)] internal class ExpressionTranslator : IExpressionTranslator, IResolvable { - private readonly IModelProvider _modelProvider; - private readonly ILinqExpressionPreprocessorComposite _preprocessor; + private readonly TranslationExpressionVisitor _translationExpressionVisitor; private readonly ISqlExpressionTranslatorComposite _translator; - private readonly IEnumerable _unknownExpressionTranslators; private readonly ILogger _logger; private readonly ConcurrentDictionary _cache; private readonly Func _factory; public ExpressionTranslator( - IModelProvider modelProvider, - ILinqExpressionPreprocessorComposite preprocessor, + TranslationExpressionVisitor translationExpressionVisitor, ISqlExpressionTranslatorComposite translator, - IEnumerable unknownExpressionTranslators, ILogger logger) { - _modelProvider = modelProvider; - _preprocessor = preprocessor; + _translationExpressionVisitor = translationExpressionVisitor; _translator = translator; - _unknownExpressionTranslators = unknownExpressionTranslators; _logger = logger; _cache = new ConcurrentDictionary(StringComparer.Ordinal); @@ -79,12 +71,7 @@ private Func TranslateSafe() private TranslatedSqlExpression TranslateUnsafe(Expression expression) { - var sqlExpression = TranslationExpressionVisitor.Translate( - new TranslationContext(), - _modelProvider, - _preprocessor, - _unknownExpressionTranslators, - expression); + var sqlExpression = _translationExpressionVisitor.Translate(new TranslationContext(), expression); return new TranslatedSqlExpression( sqlExpression.Expression, diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/BinaryExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/BinaryExpression.cs index d0306ecb..bd63fbd8 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/BinaryExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/BinaryExpression.cs @@ -19,6 +19,7 @@ public BinaryExpression( ISqlExpression right) { if (left is not BinaryExpression + && left is not ColumnsChainExpression && left is not ColumnExpression && left is not ConditionalExpression && left is not JsonAttributeExpression @@ -33,6 +34,7 @@ public BinaryExpression( } if (right is not BinaryExpression + && right is not ColumnsChainExpression && right is not ColumnExpression && right is not ConditionalExpression && right is not JsonAttributeExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs index 6c8923b7..c16cde0e 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnExpression.cs @@ -8,20 +8,26 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions /// public class ColumnExpression : ISqlExpression { + private readonly string? _nameOverride; + /// .cctor - /// Member info /// Type + /// MemberInfo /// Source + /// nameOverride public ColumnExpression( - MemberInfo member, Type type, - ISqlExpression source) + MemberInfo member, + ISqlExpression source, + string? nameOverride = null) { 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; Source = source; @@ -30,7 +36,7 @@ public ColumnExpression( /// /// Name /// - public string Name => Member.Name; + public string Name => _nameOverride ?? Member.Name; /// /// Member diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs new file mode 100644 index 00000000..deb63372 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ColumnsChainExpression.cs @@ -0,0 +1,52 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions +{ + using System; + using System.Collections.Generic; + using System.Reflection; + using Basics; + + /// + /// ColumnsChainExpression + /// + public class ColumnsChainExpression : ISqlExpression + { + /// .cctor + /// Type + /// MemberInfos + /// Source + public ColumnsChainExpression( + Type type, + IReadOnlyCollection members, + ISqlExpression source) + { + if (source is not ParameterExpression) + { + throw new ArgumentException($"{nameof(ColumnsChainExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); + } + + Members = members; + Type = type; + Source = source; + } + + /// + /// Name + /// + public string Name => Members.ToString("_", member => member.Name); + + /// + /// Member + /// + public IReadOnlyCollection Members { get; } + + /// + /// Type + /// + public Type Type { get; } + + /// + /// Source + /// + public ISqlExpression Source { get; } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ConditionalExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ConditionalExpression.cs index 60906868..5d378f98 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ConditionalExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ConditionalExpression.cs @@ -19,6 +19,7 @@ public ConditionalExpression( ISqlExpression @else) { if (when is not BinaryExpression + && when is not ColumnsChainExpression && when is not ColumnExpression && when is not ConditionalExpression && when is not JsonAttributeExpression @@ -33,6 +34,7 @@ public ConditionalExpression( } if (then is not BinaryExpression + && then is not ColumnsChainExpression && then is not ColumnExpression && then is not ConditionalExpression && then is not JsonAttributeExpression @@ -47,6 +49,7 @@ public ConditionalExpression( } if (@else is not BinaryExpression + && @else is not ColumnsChainExpression && @else is not ColumnExpression && @else is not ConditionalExpression && @else is not JsonAttributeExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs index c58dbfe9..56fa2515 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/DeleteExpression.cs @@ -8,15 +8,24 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions public class DeleteExpression : ISqlExpression { /// .cctor - /// Type - public DeleteExpression(Type type) + /// ItemType + /// FilterExpression + public DeleteExpression( + Type itemType, + FilterExpression? filterExpression) { - Type = type; + ItemType = itemType; + FilterExpression = filterExpression; } /// /// Type /// - public Type Type { get; } + public Type ItemType { get; } + + /// + /// FilterExpression + /// + public FilterExpression? FilterExpression { get; set; } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ExplainExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ExplainExpression.cs index 4f7ba1ac..e47e3d8d 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ExplainExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ExplainExpression.cs @@ -12,8 +12,7 @@ public class ExplainExpression : ISqlExpression /// Analyze public ExplainExpression(ISqlExpression source, bool analyze) { - if (source is not FilterExpression - && source is not JoinExpression + if (source is not JoinExpression && source is not NamedSourceExpression && source is not OrderByExpression && source is not ProjectionExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs index ac635c79..33c5ba05 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/FilterExpression.cs @@ -9,21 +9,13 @@ public class FilterExpression : ISqlExpression { /// .cctor /// ItemType - /// Source expression /// Predicate expression public FilterExpression( Type itemType, - ISqlExpression source, ISqlExpression predicate) { - if (source is not DeleteExpression - && source is not ProjectionExpression - && source is not SetExpression) - { - throw new ArgumentException($"{nameof(FilterExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); - } - if (predicate is not BinaryExpression + && predicate is not ColumnsChainExpression && predicate is not ColumnExpression && predicate is not ConditionalExpression && predicate is not JsonAttributeExpression @@ -38,27 +30,7 @@ public FilterExpression( } ItemType = itemType; - Source = source; Predicate = predicate; - - // TODO: simplify to assigment - /*if (Source is JoinExpression join) - { - expression = ReplaceJoinParameterExpressionsVisitor.Replace(expression, join); - } - else if (Source is ProjectionExpression projection) - { - expression = ReplaceProjectionExpressionVisitor.Compact(expression, projection); - - if (projection.Source is JoinExpression projectionJoin) - { - expression = ReplaceJoinParameterExpressionsVisitor.Replace(expression, projectionJoin); - } - } - - Predicate = Predicate != null - ? new BinaryExpression(typeof(bool), BinaryOperator.AndAlso, Predicate, expression) - : expression;*/ } /// @@ -66,11 +38,6 @@ public FilterExpression( /// public Type ItemType { get; } - /// - /// Source expression - /// - public ISqlExpression Source { get; } - /// /// Predicate expression /// diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs index d05d07a7..6c3414d1 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/InsertExpression.cs @@ -11,15 +11,15 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions public class InsertExpression : ISqlExpression { /// .cctor - /// Type + /// ItemType /// Insert behavior /// Values public InsertExpression( - Type type, + Type itemType, EnInsertBehavior insertBehavior, IReadOnlyCollection values) { - Type = type; + ItemType = itemType; InsertBehavior = insertBehavior; Values = values.ToList(); } @@ -27,7 +27,7 @@ public InsertExpression( /// /// Type /// - public Type Type { get; } + public Type ItemType { get; } /// /// Insert behavior diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JsonAttributeExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JsonAttributeExpression.cs index 75a52929..8d8ba31f 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JsonAttributeExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/JsonAttributeExpression.cs @@ -16,7 +16,8 @@ public JsonAttributeExpression( ISqlExpression source, ISqlExpression accessor) { - if (source is not ColumnExpression + if (source is not ColumnsChainExpression + && source is not ColumnExpression && source is not JsonAttributeExpression && source is not ParameterExpression && source is not ParenthesesExpression @@ -28,9 +29,6 @@ public JsonAttributeExpression( Type = type; Source = source; Accessor = accessor; - - // TODO: - throw new ArgumentException($"{nameof(JsonAttributeExpression)} doesn't support {accessor.GetType().Name} as {nameof(accessor)} argument"); } /// diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/MethodCallExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/MethodCallExpression.cs index efbdc2cc..244b7270 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/MethodCallExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/MethodCallExpression.cs @@ -21,6 +21,7 @@ public MethodCallExpression( { if (source is not null && source is not BinaryExpression + && source is not ColumnsChainExpression && source is not ColumnExpression && source is not ConditionalExpression && source is not JsonAttributeExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs index cf4f2544..cd63c77f 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NamedSourceExpression.cs @@ -8,16 +8,15 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions public class NamedSourceExpression : ISqlExpression { /// .cctor - /// Type + /// ItemType /// Source /// Parameter public NamedSourceExpression( - Type type, + Type itemType, ISqlExpression source, ParameterExpression parameter) { - if (source is not FilterExpression - && source is not JoinExpression + if (source is not JoinExpression && source is not OrderByExpression && source is not ParenthesesExpression && source is not ProjectionExpression @@ -26,20 +25,15 @@ public NamedSourceExpression( throw new ArgumentException($"{nameof(NamedSourceExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); } - Type = type; + ItemType = itemType; Source = source; Parameter = parameter; - - // TODO: remove forwarding - isn't obvious - /*context.Apply( - Source is FilterExpression filterExpression ? filterExpression.Source : Source, - expression);*/ } /// /// Type /// - public Type Type { get; } + public Type ItemType { get; } /// /// Source expression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs index ce81f782..0d788adb 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/NewExpression.cs @@ -9,18 +9,18 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions public class NewExpression : ISqlExpression { /// .cctor - /// Type + /// ItemType /// Parameters - public NewExpression(Type type, IReadOnlyCollection parameters) + public NewExpression(Type itemType, IReadOnlyCollection parameters) { - Type = type; + ItemType = itemType; Parameters = parameters; } /// /// Type /// - public Type Type { get; } + public Type ItemType { get; } /// /// Parameters diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpression.cs index eeef29fd..86243891 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpression.cs @@ -1,6 +1,5 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions { - using System; using System.Collections.Generic; using System.Linq; @@ -10,46 +9,12 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions public class OrderByExpression : ISqlExpression { /// .cctor - /// Source expression /// Order by expressions - public OrderByExpression( - ISqlExpression source, - IReadOnlyCollection expressions) + public OrderByExpression(IReadOnlyCollection expressions) { - if (source is not FilterExpression - && source is not JoinExpression - && source is not NamedSourceExpression - && source is not ProjectionExpression) - { - throw new ArgumentException($"{nameof(OrderByExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); - } - - Source = source; Expressions = expressions.ToList(); - - // TODO: simplify to assigment - /*if (Source is JoinExpression join) - { - expression = ReplaceJoinParameterExpressionsVisitor.Replace(expression, join); - } - else if (Source is ProjectionExpression projection) - { - expression = ReplaceProjectionExpressionVisitor.Compact(expression, projection); - - if (projection.Source is JoinExpression projectionJoin) - { - expression = ReplaceJoinParameterExpressionsVisitor.Replace(expression, projectionJoin); - } - } - - _expressions.Add(expression);*/ } - /// - /// Source expression - /// - public ISqlExpression Source { get; } - /// /// Order by expressions /// diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpressionExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpressionExpression.cs index a77b76c2..a1d1b611 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpressionExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/OrderByExpressionExpression.cs @@ -16,6 +16,7 @@ public OrderByExpressionExpression( EnOrderingDirection orderingDirection) { if (expression is not BinaryExpression + && expression is not ColumnsChainExpression && expression is not ColumnExpression && expression is not ConditionalExpression && expression is not JsonAttributeExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParameterExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParameterExpression.cs index 042d8335..c21b0d0b 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParameterExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParameterExpression.cs @@ -25,5 +25,10 @@ public ParameterExpression(Type type, string name) /// Name /// public string Name { get; } + + /// + /// SkipInSql + /// + public bool SkipInSql { get; set; } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParenthesesExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParenthesesExpression.cs index 60155db5..43803d85 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParenthesesExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ParenthesesExpression.cs @@ -13,11 +13,12 @@ public ParenthesesExpression(ISqlExpression source) { if (source is not BinaryExpression && source is not ConditionalExpression - && source is not FilterExpression && source is not JoinExpression + && source is not JsonAttributeExpression && source is not NamedSourceExpression && source is not OrderByExpression && source is not ProjectionExpression + && source is not QueryParameterExpression && source is not UnaryExpression) { throw new ArgumentException($"{nameof(ParenthesesExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs index 328f752e..35a7f1e4 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/ProjectionExpression.cs @@ -2,7 +2,6 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions { using System; using System.Collections.Generic; - using Basics; /// /// ProjectionExpression @@ -13,38 +12,26 @@ public class ProjectionExpression : ISqlExpression /// ItemType /// Source expression /// Expressions + /// FilterExpression + /// OrderByExpression public ProjectionExpression( Type itemType, ISqlExpression source, - IReadOnlyCollection expressions) + IReadOnlyCollection expressions, + FilterExpression? filterExpression, + OrderByExpression? orderByExpression) { - if (source is not NamedSourceExpression) + if (source is not NamedSourceExpression + && source is not JoinExpression) { throw new ArgumentException($"{nameof(ProjectionExpression)} doesn't support {source.GetType().Name} as {nameof(source)} argument"); } ItemType = itemType; Source = source; - IsProjectionToClass = itemType.IsClass && !itemType.IsPrimitive() && !itemType.IsCollection(); - IsAnonymousProjection = itemType.IsCompilerGenerated(); Expressions = expressions; - - // todo: new expression - /*IsProjectionToClass = true; - IsAnonymousProjection = expression.Type.IsCompilerGenerated();*/ - - // TODO: simplify to assigment - /*if (Source is JoinExpression join) - { - expression = ReplaceJoinParameterExpressionsVisitor.Replace(expression, join); - } - - if (expression is ParameterExpression) - { - return; - } - - _expressions.Add(expression);*/ + FilterExpression = filterExpression; + OrderByExpression = orderByExpression; } /// @@ -52,16 +39,6 @@ public ProjectionExpression( /// public Type ItemType { get; } - /// - /// Is projection creates anonymous or user defined class - /// - public bool IsProjectionToClass { get; } - - /// - /// Is projection creates anonymous class - /// - public bool IsAnonymousProjection { get; } - /// /// Is projection takes distinct values /// @@ -76,5 +53,15 @@ public ProjectionExpression( /// Expressions /// public IReadOnlyCollection Expressions { get; } + + /// + /// FilterExpression + /// + public FilterExpression? FilterExpression { get; set; } + + /// + /// OrderByExpression + /// + public OrderByExpression? OrderByExpression { get; set; } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs index 1ab370ff..ac1c8102 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RenameExpression.cs @@ -17,6 +17,7 @@ public RenameExpression( ISqlExpression source) { if (source is not BinaryExpression + && source is not ColumnsChainExpression && source is not ColumnExpression && source is not ConditionalExpression && source is not JsonAttributeExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RowsFetchLimitExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RowsFetchLimitExpression.cs index 256c96e2..e66d036f 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RowsFetchLimitExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/RowsFetchLimitExpression.cs @@ -12,8 +12,7 @@ public class RowsFetchLimitExpression : ISqlExpression /// Limit public RowsFetchLimitExpression(ISqlExpression source, uint rowsFetchLimit) { - if (source is not FilterExpression - && source is not JoinExpression + if (source is not JoinExpression && source is not NamedSourceExpression && source is not ProjectionExpression) { diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/SetExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/SetExpression.cs deleted file mode 100644 index b35f6499..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/SetExpression.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions -{ - using System.Collections.Generic; - - /// - /// SetExpression - /// - public class SetExpression : ISqlExpression - { - /// .cctor - /// Source - /// Assignments - public SetExpression( - UpdateExpression source, - IReadOnlyCollection assignments) - { - Source = source; - Assignments = assignments; - } - - /// - /// Source - /// - public ISqlExpression Source { get; } - - /// - /// Assignment - /// - public IReadOnlyCollection Assignments { get; } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UnaryExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UnaryExpression.cs index dec4ee0d..450e493e 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UnaryExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UnaryExpression.cs @@ -17,6 +17,7 @@ public UnaryExpression( ISqlExpression source) { if (source is not BinaryExpression + && source is not ColumnsChainExpression && source is not ColumnExpression && source is not ConditionalExpression && source is not JsonAttributeExpression diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs index 33890463..c4987ef9 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/Expressions/UpdateExpression.cs @@ -1,6 +1,7 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions { using System; + using System.Collections.Generic; /// /// UpdateExpression @@ -8,15 +9,32 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.Expressions public class UpdateExpression : ISqlExpression { /// .cctor - /// Type - public UpdateExpression(Type type) + /// ItemType + /// Assignments + /// FilterExpression + public UpdateExpression( + Type itemType, + IReadOnlyCollection assignments, + FilterExpression? filterExpression) { - Type = type; + ItemType = itemType; + FilterExpression = filterExpression; + Assignments = assignments; } /// /// Type /// - public Type Type { get; } + public Type ItemType { get; } + + /// + /// Assignment + /// + public IReadOnlyCollection Assignments { get; } + + /// + /// FilterExpression + /// + public FilterExpression? FilterExpression { get; set; } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ExtractRelationsExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ExtractRelationsExpressionVisitor.cs deleted file mode 100644 index d9053791..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/ExtractRelationsExpressionVisitor.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation -{ - using System.Collections.Generic; - using System.Linq.Expressions; - using System.Reflection; - using Basics; - using Model; - - internal class ExtractRelationsExpressionVisitor : ExpressionVisitor - { - private readonly IModelProvider _modelProvider; - private readonly HashSet _relations; - - private ExtractRelationsExpressionVisitor(IModelProvider modelProvider) - { - _modelProvider = modelProvider; - _relations = new HashSet(); - } - - public static IReadOnlyCollection Extract(Expression node, IModelProvider modelProvider) - { - var visitor = new ExtractRelationsExpressionVisitor(modelProvider); - - _ = visitor.Visit(node); - - return visitor._relations; - } - - protected override Expression VisitMember(MemberExpression node) - { - if (node.Member is PropertyInfo property - && node.Type.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>))) - { - _relations.Add(new Relation(node.Expression.Type, node.Type, new ColumnProperty(property, property), _modelProvider)); - } - - return base.VisitMember(node); - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/FirstLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/FirstLinqExpressionVisitor.cs new file mode 100644 index 00000000..4c16f58d --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/FirstLinqExpressionVisitor.cs @@ -0,0 +1,41 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class FirstLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableFirst() + || method == LinqMethods.QueryableFirstOrDefault()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var source = context.SqlExpression; + var rowsFetchLimitExpression = new RowsFetchLimitExpression(source, 1); + context.Remember(rowsFetchLimitExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/GetJsonAttributeUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/GetJsonAttributeLinqExpressionVisitor.cs similarity index 59% rename from src/Modules/DataAccess.Orm.Sql/Translation/GetJsonAttributeUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/GetJsonAttributeLinqExpressionVisitor.cs index d7e64767..04447f32 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/GetJsonAttributeUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/GetJsonAttributeLinqExpressionVisitor.cs @@ -7,25 +7,30 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Basics; using Expressions; using Linq; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class GetJsonAttributeUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class GetJsonAttributeLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.GetJsonAttribute()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.GetJsonAttribute()) { visitor.Visit(methodCallExpression.Arguments[0]); var source = context.SqlExpression; visitor.Visit(methodCallExpression.Arguments[1]); var accessor = context.SqlExpression; - var jsonAttributeExpression = new JsonAttributeExpression(expression.Type, source, accessor); + var jsonAttributeExpression = new JsonAttributeExpression(methodCallExpression.Type, source, accessor); var parenthesesExpression = new ParenthesesExpression(jsonAttributeExpression); context.Remember(parenthesesExpression); diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/HasJsonAttributeUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/HasJsonAttributeLinqExpressionVisitor.cs similarity index 50% rename from src/Modules/DataAccess.Orm.Sql/Translation/HasJsonAttributeUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/HasJsonAttributeLinqExpressionVisitor.cs index 41c14780..7f48d049 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/HasJsonAttributeUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/HasJsonAttributeLinqExpressionVisitor.cs @@ -7,26 +7,30 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Basics; using Expressions; using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class HasJsonAttributeUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class HasJsonAttributeLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.GenericMethodDefinitionOrSelf() == LinqMethods.HasJsonAttribute()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.HasJsonAttribute()) { visitor.Visit(methodCallExpression.Arguments[0]); var left = context.SqlExpression; visitor.Visit(methodCallExpression.Arguments[1]); var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(void), BinaryOperator.HasJsonAttribute, left, right); + var binaryExpression = new Expressions.BinaryExpression(typeof(void), BinaryOperator.HasJsonAttribute, left, right); context.Remember(binaryExpression); return true; diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/IUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ILinqExpressionVisitor.cs similarity index 72% rename from src/Modules/DataAccess.Orm.Sql/Translation/IUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/ILinqExpressionVisitor.cs index aa41b114..76740819 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/IUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ILinqExpressionVisitor.cs @@ -3,20 +3,20 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using System.Linq.Expressions; /// - /// IMemberInfoTranslator + /// IMethodCallExpressionTranslator /// - public interface IUnknownExpressionTranslator + public interface ILinqExpressionVisitor { /// /// Translates sql expression /// + /// Visitor /// TranslationContext /// Expression - /// Visitor /// Recognition result - bool TryTranslate( + bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor); + Expression expression); } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Model/ISqlViewQueryProvider.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ISqlViewQueryProvider.cs similarity index 92% rename from src/Modules/DataAccess.Orm.Sql/Model/ISqlViewQueryProvider.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/ISqlViewQueryProvider.cs index 2f7c24a3..4e3a37b6 100644 --- a/src/Modules/DataAccess.Orm.Sql/Model/ISqlViewQueryProvider.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ISqlViewQueryProvider.cs @@ -1,6 +1,7 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Model +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation { using System; + using Model; /// /// ISqlViewQueryProvider diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/InsertLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/InsertLinqExpressionVisitor.cs new file mode 100644 index 00000000..62c72f8b --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/InsertLinqExpressionVisitor.cs @@ -0,0 +1,173 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + using Model; + using Transaction; + + [Component(EnLifestyle.Singleton)] + internal class InsertLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + private readonly IModelProvider _modelProvider; + + public InsertLinqExpressionVisitor(IModelProvider modelProvider) + { + _modelProvider = modelProvider; + } + + internal static MethodInfo GetInsertValuesMethod { get; } = new MethodFinder( + typeof(InsertLinqExpressionVisitor), + nameof(GetInsertValues), + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod) + { + ArgumentTypes = new[] { typeof(IModelProvider), typeof(IReadOnlyCollection) } + } + .FindMethod() ?? throw new InvalidOperationException("SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.InsertMethodCallExpressionTranslator.GetInsertValues()"); + + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.RepositoryInsert()) + { + var getInsertValuesMethodCallExpression = Expression.Call( + null, + GetInsertValuesMethod, + Expression.Constant(_modelProvider), + Expression.Constant(new List(), typeof(IReadOnlyCollection))); + + using (context.WithinPathScope(methodCallExpression.Arguments[1])) + using (context.WithinPathScope(getInsertValuesMethodCallExpression)) + { + _ = (IAdvancedDatabaseTransaction)((ConstantExpression)methodCallExpression.Arguments[0]).Value; + var entities = (IReadOnlyCollection)((ConstantExpression)methodCallExpression.Arguments[1]).Value; + var insertBehavior = (EnInsertBehavior)((ConstantExpression)methodCallExpression.Arguments[2]).Value; + + var map = entities + .SelectMany(_modelProvider.Flatten) + .Distinct(new UniqueIdentifiedEqualityComparer()) + .ToDictionary(entity => new EntityKey(entity), entity => entity); + + var stacks = map + .Values + .OrderByDependencies(entity => new EntityKey(entity), GetDependencies(_modelProvider, map)) + .Stack(entity => entity.GetType()); + + var expressions = stacks + .Select(pair => + { + var (type, stack) = pair; + var table = _modelProvider.Tables[type]; + + var values = stack + .Select(_ => + { + var values = table + .Columns + .Values + .Where(column => !column.IsMultipleRelation) + .Select(column => + { + var name = context.NextCommandParameterName(); + context.CaptureCommandParameterExtractor(name, null); + return new QueryParameterExpression(column.Type, name); + }) + .ToList(); + + return new ValuesExpression(values); + }) + .ToList(); + + return new InsertExpression(type, insertBehavior, values); + }) + .ToList(); + + var batchExpression = new BatchExpression(expressions); + context.Remember(batchExpression); + } + + return true; + } + + return false; + } + + private static Func> GetDependencies( + IModelProvider modelProvider, + IReadOnlyDictionary map) + { + return entity => + { + var table = modelProvider.Tables[entity.GetType()]; + + return table + .Columns + .Values + .Where(column => column.IsRelation) + .Select(DependencySelector(table, entity, map)) + .Where(dependency => dependency != null) + .Select(dependency => dependency!); + + static Func DependencySelector( + ITableInfo table, + IUniqueIdentified entity, + IReadOnlyDictionary map) + { + return column => table.IsMtmTable + ? map[new EntityKey(column.Relation.Target, column.GetValue(entity) !)] + : column.GetRelationValue(entity); + } + }; + } + + private static IReadOnlyDictionary GetInsertValues( + IModelProvider modelProvider, + IReadOnlyCollection entities) + { + var map = entities + .SelectMany(modelProvider.Flatten) + .Distinct(new UniqueIdentifiedEqualityComparer()) + .ToDictionary(entity => new EntityKey(entity), entity => entity); + + return map + .Values + .OrderByDependencies(entity => new EntityKey(entity), GetDependencies(modelProvider, map)) + .SelectMany(entity => modelProvider + .Tables[entity.GetType()] + .Columns + .Values + .Where(column => !column.IsMultipleRelation) + .Select(column => + { + var value = column.GetValue(entity); + + return column.IsJsonColumn + ? Expression.Constant(new DatabaseJsonObject(value, column.Type), typeof(DatabaseJsonObject)) + : Expression.Constant(value, column.Type); + })) + .Select((expression, index) => (name: TranslationContext.CommandParameterFormat.Format(index), expression)) + .ToDictionary( + pair => pair.name, + pair => pair.expression, + StringComparer.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullLinqExpressionVisitor.cs new file mode 100644 index 00000000..d51f96e2 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullLinqExpressionVisitor.cs @@ -0,0 +1,37 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class IsNotNullLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + if (methodCallExpression.Method == LinqMethods.IsNotNull()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var left = context.SqlExpression; + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.IsNot, left, new NullExpression()); + context.Remember(binaryExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullUnknownExpressionTranslator.cs deleted file mode 100644 index 3ac9c064..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/IsNotNullUnknownExpressionTranslator.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation -{ - using System.Linq.Expressions; - using AutoRegistration.Api.Abstractions; - using AutoRegistration.Api.Attributes; - using AutoRegistration.Api.Enumerations; - using Expressions; - using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; - - [Component(EnLifestyle.Singleton)] - internal class IsNotNullUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable - { - public bool TryTranslate( - TranslationContext context, - Expression expression, - ExpressionVisitor visitor) - { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method == LinqMethods.IsNotNull()) - { - visitor.Visit(methodCallExpression.Arguments[0]); - var left = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(bool), BinaryOperator.IsNot, left, new NullExpression()); - context.Remember(binaryExpression); - - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/IsNullLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/IsNullLinqExpressionVisitor.cs new file mode 100644 index 00000000..20d09be2 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/IsNullLinqExpressionVisitor.cs @@ -0,0 +1,37 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class IsNullLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + if (methodCallExpression.Method == LinqMethods.IsNull()) + { + visitor.Visit(methodCallExpression.Arguments); + var left = context.SqlExpression; + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Is, left, new NullExpression()); + context.Remember(binaryExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/IsNullUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/IsNullUnknownExpressionTranslator.cs deleted file mode 100644 index 84560d53..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/IsNullUnknownExpressionTranslator.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation -{ - using System.Linq.Expressions; - using AutoRegistration.Api.Abstractions; - using AutoRegistration.Api.Attributes; - using AutoRegistration.Api.Enumerations; - using Expressions; - using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; - - [Component(EnLifestyle.Singleton)] - internal class IsNullUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable - { - public bool TryTranslate( - TranslationContext context, - Expression expression, - ExpressionVisitor visitor) - { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method == LinqMethods.IsNull()) - { - visitor.Visit(methodCallExpression.Arguments); - var left = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(bool), BinaryOperator.Is, left, new NullExpression()); - context.Remember(binaryExpression); - - return true; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/LikeUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/LikeLinqExpressionVisitor.cs similarity index 52% rename from src/Modules/DataAccess.Orm.Sql/Translation/LikeUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/LikeLinqExpressionVisitor.cs index 0e8b0cba..3c00590a 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/LikeUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/LikeLinqExpressionVisitor.cs @@ -6,26 +6,28 @@ using AutoRegistration.Api.Enumerations; using Expressions; using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class LikeUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class LikeLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { - if (expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method == LinqMethods.Like()) + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + if (methodCallExpression.Method == LinqMethods.Like()) { visitor.Visit(methodCallExpression.Arguments[0]); var left = context.SqlExpression; visitor.Visit(methodCallExpression.Arguments[1]); var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(bool), BinaryOperator.Like, left, right); + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Like, left, right); context.Remember(binaryExpression); return true; diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsLinqExpressionVisitor.cs new file mode 100644 index 00000000..c2592e92 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsLinqExpressionVisitor.cs @@ -0,0 +1,60 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Linq.Expressions; + using System.Reflection; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class ObjectEqualsLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + if (methodCallExpression.Method == LinqMethods.ObjectEquals()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var left = context.SqlExpression; + visitor.Visit(methodCallExpression.Arguments[1]); + var right = context.SqlExpression; + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Equal, left, right); + context.Remember(binaryExpression); + + return true; + } + + if (IsInstanceEquals(methodCallExpression.Method)) + { + visitor.Visit(methodCallExpression.Object); + var left = context.SqlExpression; + visitor.Visit(methodCallExpression.Arguments[0]); + var right = context.SqlExpression; + var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Equal, left, right); + context.Remember(binaryExpression); + + return true; + } + + return false; + } + + private static bool IsInstanceEquals(MethodInfo method) + { + return !method.IsStatic + && method.DeclaringType == typeof(object) + && method.Name.Equals(nameof(object.Equals), StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsUnknownExpressionTranslator.cs deleted file mode 100644 index 50b06446..00000000 --- a/src/Modules/DataAccess.Orm.Sql/Translation/ObjectEqualsUnknownExpressionTranslator.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation -{ - using System; - using System.Linq.Expressions; - using System.Reflection; - using AutoRegistration.Api.Abstractions; - using AutoRegistration.Api.Attributes; - using AutoRegistration.Api.Enumerations; - using Expressions; - using Linq; - using BinaryExpression = Expressions.BinaryExpression; - using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; - - [Component(EnLifestyle.Singleton)] - internal class ObjectEqualsUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable - { - public bool TryTranslate( - TranslationContext context, - Expression expression, - ExpressionVisitor visitor) - { - if (expression is MethodCallExpression methodCallExpression) - { - if (methodCallExpression.Method == LinqMethods.ObjectEquals()) - { - visitor.Visit(methodCallExpression.Arguments[0]); - var left = context.SqlExpression; - visitor.Visit(methodCallExpression.Arguments[1]); - var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(bool), BinaryOperator.Equal, left, right); - context.Remember(binaryExpression); - - return true; - } - - if (IsInstanceEquals(methodCallExpression.Method)) - { - visitor.Visit(methodCallExpression.Object); - var left = context.SqlExpression; - visitor.Visit(methodCallExpression.Arguments[0]); - var right = context.SqlExpression; - var binaryExpression = new BinaryExpression(typeof(bool), BinaryOperator.Equal, left, right); - context.Remember(binaryExpression); - - return true; - } - } - - return false; - - static bool IsInstanceEquals(MethodInfo method) - { - return !method.IsStatic - && method.DeclaringType == typeof(object) - && method.Name.Equals(nameof(object.Equals), StringComparison.OrdinalIgnoreCase); - } - } - } -} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs new file mode 100644 index 00000000..efae6f89 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/OrderByLinqExpressionVisitor.cs @@ -0,0 +1,64 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Basics.Enumerations; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class OrderByLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableOrderBy() + || method == LinqMethods.QueryableOrderByDescending() + || method == LinqMethods.QueryableThenBy() + || method == LinqMethods.QueryableThenByDescending()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var source = context.SqlExpression; + + if (source is ProjectionExpression { Source: NamedSourceExpression namedSourceExpression } projectionExpression) + { + var parameterExpression = namedSourceExpression.Parameter; + ISqlExpression orderByExpressionAccessor; + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + orderByExpressionAccessor = context.SqlExpression; + } + + var direction = method == LinqMethods.QueryableOrderBy() || 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); + context.Remember(projectionExpression); + + return true; + } + } + + return false; + } + } +} \ 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 new file mode 100644 index 00000000..6e75f459 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/RepositoryAllLinqExpressionVisitor.cs @@ -0,0 +1,122 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + using Model; + + [Component(EnLifestyle.Singleton)] + internal class RepositoryAllLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + private readonly IModelProvider _modelProvider; + + public RepositoryAllLinqExpressionVisitor(IModelProvider modelProvider) + { + _modelProvider = modelProvider; + } + + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.RepositoryAll()) + { + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + if (context.IsOuterExpression() && !itemType.IsSqlView()) + { + var querySourceExpression = new QuerySourceExpression(itemType); + + if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, querySourceExpression) is { } joinProjectionExpression) + { + var projectionExpression = joinProjectionExpression; + context.Remember(projectionExpression); + } + 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); + } + } + else + { + var querySourceExpression = new QuerySourceExpression(itemType); + + if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, querySourceExpression) is { } projectionExpression) + { + context.Remember(projectionExpression); + } + else + { + context.Remember(querySourceExpression); + } + } + + return true; + } + + 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 new file mode 100644 index 00000000..1565e3a0 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/SelectLinqExpressionVisitor.cs @@ -0,0 +1,238 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + using Model; + + [Component(EnLifestyle.Singleton)] + internal class SelectLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + private readonly IModelProvider _modelProvider; + + public SelectLinqExpressionVisitor(IModelProvider modelProvider) + { + _modelProvider = modelProvider; + } + + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableSelect()) + { + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + visitor.Visit(methodCallExpression.Arguments[0]); + 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 namedSourceExpression = new NamedSourceExpression(sourceItemType, unnamedSource, parameterExpression); + + IReadOnlyCollection expressions; + + using (context.OpenParameterScope(namedSourceExpression.Parameter)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + expressions = context.SqlExpression is Expressions.NewExpression newExpression + ? newExpression.Parameters + : 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 projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, null, null); + + if (BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + { + projectionExpression = joinProjectionExpression; + } + + context.Remember(projectionExpression); + + return true; + } + + return false; + } + + // TODO: private + internal static ISqlExpression? GetSource(ISqlExpression source) + { + return source switch + { + DeleteExpression => default, + JoinExpression => default, + NamedSourceExpression namedSourceExpression => namedSourceExpression.Source, + ProjectionExpression projectionExpression => GetSource(projectionExpression.Source), + QuerySourceExpression => default, + UpdateExpression => default, + _ => throw new NotSupportedException(source.GetType().FullName) + }; + } + + // TODO: private + internal static Type GetSourceItemType(ISqlExpression source) + { + return source switch + { + DeleteExpression deleteExpression => deleteExpression.ItemType, + ParenthesesExpression parenthesesExpression => GetSourceItemType(parenthesesExpression.Source), + ProjectionExpression projectionExpression => projectionExpression.ItemType, + QuerySourceExpression querySourceExpression => querySourceExpression.ItemType, + UpdateExpression updateExpression => updateExpression.ItemType, + _ => throw new NotSupportedException(source.GetType().FullName) + }; + } + + // TODO: private + internal static ProjectionExpression? BuildJoinExpression( + TranslationContext context, + IModelProvider modelProvider, + ISqlExpression sourceExpression) + { + var sourceItemType = GetSourceItemType(sourceExpression); + + if (!sourceItemType.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>))) + { + return null; + } + + var relations = modelProvider + .Tables[sourceItemType] + .Columns + .Select(it => it.Value) + .Where(column => column.IsRelation) + .Select(column => column.Relation) + .ToList(); + + if (!relations.Any()) + { + return null; + } + + var sourceParameterExpression = new Expressions.ParameterExpression(sourceItemType, context.NextLambdaParameterName()); + NamedSourceExpression sourceNamedSourceExpression; + IReadOnlyCollection sourceExpressions; + FilterExpression? filterExpression; + OrderByExpression? orderByExpression; + + switch (sourceExpression) + { + 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(); + + foreach (var relation in relations) + { + var targetItemType = relation.Target; + var targetQuerySourceExpression = new QuerySourceExpression(targetItemType); + var targetParameterExpression = new Expressions.ParameterExpression(targetItemType, context.NextLambdaParameterName()); + 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) + .ToList(); + var onExpression = BuildJoinOnExpression(modelProvider, relation, sourceParameterExpression, targetParameterExpression); + joinAccumulator = new JoinExpression(joinAccumulator, targetNamedSourceExpression, onExpression); + + expressions = expressions.Concat(targetExpressions); + } + + var projectionExpression = new ProjectionExpression(null!, joinAccumulator, expressions.ToList(), filterExpression, orderByExpression); + + return projectionExpression; + + static Expressions.BinaryExpression BuildJoinOnExpression( + IModelProvider modelProvider, + Relation relation, + Expressions.ParameterExpression sourceParameterExpression, + Expressions.ParameterExpression targetParameterExpression) + { + var targetPrimaryKeyColumn = modelProvider + .Tables[relation.Target] + .Columns[nameof(IUniqueIdentified.PrimaryKey)]; + + return new Expressions.BinaryExpression( + typeof(bool), + BinaryOperator.Equal, + new ColumnExpression( + targetPrimaryKeyColumn.Type, + relation.Target.Column(nameof(IUniqueIdentified.PrimaryKey)).Reflected, + targetParameterExpression), + new ColumnExpression( + targetPrimaryKeyColumn.Type, + relation.Property.Reflected, + sourceParameterExpression)); + } + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/SetLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/SetLinqExpressionVisitor.cs new file mode 100644 index 00000000..a3b7683b --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/SetLinqExpressionVisitor.cs @@ -0,0 +1,44 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class SetLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.RepositoryUpdateSet() + || method == LinqMethods.RepositoryChainedUpdateSet()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var updateExpression = (UpdateExpression)context.SqlExpression; + visitor.Visit(methodCallExpression.Arguments[1]); + var assignment = (Expressions.BinaryExpression)context.SqlExpression; + ((ICollection)updateExpression.Assignments).Add(assignment); + context.Remember(updateExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/SingleLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/SingleLinqExpressionVisitor.cs new file mode 100644 index 00000000..c306ec73 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/SingleLinqExpressionVisitor.cs @@ -0,0 +1,41 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class SingleLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableSingle() + || method == LinqMethods.QueryableSingleOrDefault()) + { + visitor.Visit(methodCallExpression.Arguments[0]); + var source = context.SqlExpression; + var rowsFetchLimitExpression = new RowsFetchLimitExpression(source, 2); + context.Remember(rowsFetchLimitExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpression.cs b/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpression.cs index a3c4cdc3..24509188 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpression.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpression.cs @@ -5,9 +5,12 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using System.Linq.Expressions; using Expressions; - internal class SqlExpression + /// + /// SqlExpression + /// + public class SqlExpression { - public SqlExpression( + internal SqlExpression( ISqlExpression expression, Func> commandParametersExtractor) { @@ -15,8 +18,14 @@ public SqlExpression( CommandParametersExtractor = commandParametersExtractor; } + /// + /// Expression + /// public ISqlExpression Expression { get; } + /// + /// CommandParametersExtractor + /// public Func> CommandParametersExtractor { get; } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpressionTranslatorComposite.cs b/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpressionTranslatorComposite.cs index e3fcf915..3e4a8fde 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpressionTranslatorComposite.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/SqlExpressionTranslatorComposite.cs @@ -7,27 +7,24 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using Basics; - using CompositionRoot; using Expressions; [Component(EnLifestyle.Singleton)] internal class SqlExpressionTranslatorComposite : ISqlExpressionTranslatorComposite, IResolvable { - private readonly IDependencyContainer _dependencyContainer; + private readonly IEnumerable _sqlExpressionTranslators; private IReadOnlyDictionary? _map; - public SqlExpressionTranslatorComposite(IDependencyContainer dependencyContainer) + public SqlExpressionTranslatorComposite(IEnumerable sqlExpressionTranslators) { - _dependencyContainer = dependencyContainer; + _sqlExpressionTranslators = sqlExpressionTranslators; } public string Translate(ISqlExpression expression, int depth) { - _map ??= _dependencyContainer - .ResolveCollection() - .ToDictionary(static translator => translator.GetType().ExtractGenericArgumentAt(typeof(ISqlExpressionTranslator<>))); + _map ??= _sqlExpressionTranslators.ToDictionary(static translator => translator.GetType().ExtractGenericArgumentAt(typeof(ISqlExpressionTranslator<>))); return _map.TryGetValue(expression.GetType(), out var translator) ? translator.Translate(expression, depth) diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/StringEmptyUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/StringEmptyLinqExpressionVisitor.cs similarity index 77% rename from src/Modules/DataAccess.Orm.Sql/Translation/StringEmptyUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/StringEmptyLinqExpressionVisitor.cs index 45f3046c..ea1cc071 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/StringEmptyUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/StringEmptyLinqExpressionVisitor.cs @@ -9,13 +9,13 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using Linq; [Component(EnLifestyle.Singleton)] - internal class StringEmptyUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class StringEmptyLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { if (expression is MemberExpression memberExpression && memberExpression.Member == LinqMethods.StringEmpty()) diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/StringLengthUnknownExpressionTranslator.cs b/src/Modules/DataAccess.Orm.Sql/Translation/StringLengthLinqExpressionVisitor.cs similarity index 62% rename from src/Modules/DataAccess.Orm.Sql/Translation/StringLengthUnknownExpressionTranslator.cs rename to src/Modules/DataAccess.Orm.Sql/Translation/StringLengthLinqExpressionVisitor.cs index 16353084..9124849c 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/StringLengthUnknownExpressionTranslator.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/StringLengthLinqExpressionVisitor.cs @@ -6,24 +6,23 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using AutoRegistration.Api.Attributes; using AutoRegistration.Api.Enumerations; using Linq; - using MethodCallExpression = Expressions.MethodCallExpression; [Component(EnLifestyle.Singleton)] - internal class StringLengthUnknownExpressionTranslator : IUnknownExpressionTranslator, - ICollectionResolvable + internal class StringLengthLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable { [SuppressMessage("Analysis", "CA1308", Justification = "sql script readability")] - public bool TryTranslate( + public bool TryVisit( + ExpressionVisitor visitor, TranslationContext context, - Expression expression, - ExpressionVisitor visitor) + Expression expression) { if (expression is MemberExpression memberExpression && memberExpression.Member == LinqMethods.StringLength()) { visitor.Visit(memberExpression.Expression); var argument = context.SqlExpression; - var methodCallExpression = new MethodCallExpression(typeof(int), nameof(string.Length).ToLowerInvariant(), null, new[] { argument }); + var methodCallExpression = new Expressions.MethodCallExpression(typeof(int), nameof(string.Length).ToLowerInvariant(), null, new[] { argument }); context.Remember(methodCallExpression); return true; diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs index 60ae9ce1..eab4d2ee 100644 --- a/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs +++ b/src/Modules/DataAccess.Orm.Sql/Translation/TranslationExpressionVisitor.cs @@ -2,15 +2,14 @@ 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 System.Reflection; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; using Basics; using Expressions; - using Linq; - using Model; - using Transaction; using BinaryExpression = System.Linq.Expressions.BinaryExpression; using ConditionalExpression = System.Linq.Expressions.ConditionalExpression; using MethodCallExpression = System.Linq.Expressions.MethodCallExpression; @@ -18,915 +17,234 @@ namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation using ParameterExpression = System.Linq.Expressions.ParameterExpression; using UnaryExpression = System.Linq.Expressions.UnaryExpression; - [SuppressMessage("Analysis", "CA1502", Justification = "complex infrastructural code")] - [SuppressMessage("Analysis", "CA1506", Justification = "complex infrastructural code")] - internal class TranslationExpressionVisitor : ExpressionVisitor + /// + /// TranslationExpressionVisitor + /// + [Component(EnLifestyle.Singleton)] + public class TranslationExpressionVisitor : ExpressionVisitor, + IResolvable { - private readonly Expression _expression; - private readonly TranslationContext _context; - private readonly IModelProvider _modelProvider; private readonly ILinqExpressionPreprocessorComposite _preprocessor; - private readonly IEnumerable _unknownExpressionTranslators; + private readonly IEnumerable _linqExpressionVisitors; - private TranslationExpressionVisitor( - Expression expression, - TranslationContext context, - IModelProvider modelProvider, + /// .cctor + /// ILinqExpressionPreprocessorComposite + /// ILinqExpressionVisitor + public TranslationExpressionVisitor( ILinqExpressionPreprocessorComposite preprocessor, - IEnumerable unknownExpressionTranslators) + IEnumerable linqExpressionVisitors) { - _expression = expression; - _context = context; - _modelProvider = modelProvider; _preprocessor = preprocessor; - _unknownExpressionTranslators = unknownExpressionTranslators; + _linqExpressionVisitors = linqExpressionVisitors; } - internal static MethodInfo GetInsertValuesMethod { get; } = new MethodFinder( - typeof(TranslationExpressionVisitor), - nameof(GetInsertValues), - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.InvokeMethod) - { - ArgumentTypes = new[] { typeof(IModelProvider), typeof(IReadOnlyCollection) } - } - .FindMethod() ?? throw new InvalidOperationException("SpaceEngineers.Core.DataAccess.Orm.Sql.Translation.TranslationExpressionVisitor.GetInsertValues()"); - - public static SqlExpression Translate( + /// + /// Translates System.Linq.Expressions.Expression to SqlExpression + /// + /// TranslationContext + /// Expression + /// SqlExpression + public SqlExpression Translate( TranslationContext context, - IModelProvider modelProvider, - ILinqExpressionPreprocessorComposite preprocessor, - IEnumerable unknownExpressionTranslators, Expression expression) { - var visitor = new TranslationExpressionVisitor( - expression, - context, - modelProvider, - preprocessor, - unknownExpressionTranslators); - - visitor.Visit(preprocessor.Visit(expression)); - - return new SqlExpression( - visitor._context.SqlExpression ?? throw new InvalidOperationException("Sql expression wasn't built"), - visitor._context.BuildCommandParametersExtractor(preprocessor)); - } + var visitor = new InternalTranslationExpressionVisitor(this, context, expression); - public sealed override Expression Visit(Expression node) - { - using (_context.WithinPathScope(node)) - { - base.Visit(node); - - return node; - } + return visitor.Translate(); } - protected override Expression VisitMethodCall(MethodCallExpression node) + private class InternalTranslationExpressionVisitor : ExpressionVisitor { - var method = node.Method.GenericMethodDefinitionOrSelf(); - - var itemType = node.Type.ExtractQueryableItemType(); + private readonly TranslationExpressionVisitor _visitor; + private readonly TranslationContext _context; + private readonly Expression _expression; - if (method == LinqMethods.CachedExpression() - || method == LinqMethods.CachedInsertExpression() - || method == LinqMethods.CachedUpdateExpression() - || method == LinqMethods.CachedDeleteExpression() - || method == LinqMethods.WithDependencyContainer()) + internal InternalTranslationExpressionVisitor( + TranslationExpressionVisitor visitor, + TranslationContext context, + Expression expression) { - Visit(node.Arguments[0]); - - return node; + _visitor = visitor; + _context = context; + _expression = expression; } - if (method == LinqMethods.RepositoryInsert()) + public sealed override Expression Visit(Expression node) { - var getInsertValuesMethodCallExpression = Expression.Call( - null, - GetInsertValuesMethod, - Expression.Constant(_modelProvider), - Expression.Constant(new List(), typeof(IReadOnlyCollection))); - - using (_context.WithinPathScope(node.Arguments[1])) - using (_context.WithinPathScope(getInsertValuesMethodCallExpression)) + using (_context.WithinPathScope(node)) { - _ = (IAdvancedDatabaseTransaction)((ConstantExpression)node.Arguments[0]).Value; - var entities = (IReadOnlyCollection)((ConstantExpression)node.Arguments[1]).Value; - var insertBehavior = (EnInsertBehavior)((ConstantExpression)node.Arguments[2]).Value; - - var map = entities - .SelectMany(_modelProvider.Flatten) - .Distinct(new UniqueIdentifiedEqualityComparer()) - .ToDictionary(entity => new EntityKey(entity), entity => entity); - - var stacks = map - .Values - .OrderByDependencies(entity => new EntityKey(entity), GetDependencies(_modelProvider, map)) - .Stack(entity => entity.GetType()); - - var expressions = stacks - .Select(pair => - { - var (type, stack) = pair; - var table = _modelProvider.Tables[type]; - - var values = stack - .Select(_ => - { - var values = table - .Columns - .Values - .Where(column => !column.IsMultipleRelation) - .Select(column => - { - var name = _context.NextCommandParameterName(); - _context.CaptureCommandParameterExtractor(name, null); - return new QueryParameterExpression(column.Type, name); - }) - .ToList(); - - return new ValuesExpression(values); - }) - .ToList(); - - return new InsertExpression(type, insertBehavior, values); - }) - .ToList(); - - var batchExpression = new BatchExpression(expressions); - _context.Remember(batchExpression); - } + base.Visit(node); - return node; - } - - if (method == LinqMethods.RepositoryUpdate()) - { - var updateExpression = new UpdateExpression(itemType); - _context.Remember(updateExpression); - - return node; + return node; + } } - if (method == LinqMethods.RepositoryUpdateSet() - || method == LinqMethods.RepositoryChainedUpdateSet()) + internal SqlExpression Translate() { - Visit(node.Arguments[0]); - var source = (UpdateExpression)_context.SqlExpression; - Visit(node.Arguments[1]); - var assignments = (Expressions.BinaryExpression)_context.SqlExpression; - var setExpression = new SetExpression(source, new[] { assignments }); - _context.Remember(setExpression); + Visit(_visitor._preprocessor.Visit(_expression)); - return node; + return new SqlExpression( + _context.SqlExpression ?? throw new InvalidOperationException("Sql expression wasn't built"), + _context.BuildCommandParametersExtractor(_visitor._preprocessor)); } - if (method == LinqMethods.RepositoryDelete()) + protected override Expression VisitMethodCall(MethodCallExpression node) { - var deleteExpression = new DeleteExpression(itemType); - _context.Remember(deleteExpression); + if (TryVisitLinqExpression(node)) + { + return node; + } - return node; + throw new NotSupportedException($"method: {node.Method}"); } - if (method == LinqMethods.RepositoryAll()) + protected override Expression VisitMember(MemberExpression node) { - if (_context.IsOuterExpression() && !itemType.IsSqlView()) + if (TryVisitLinqExpression(node)) { - var querySourceExpression = new QuerySourceExpression(itemType); - var parameterExpression = new Expressions.ParameterExpression(itemType, _context.NextLambdaParameterName()); - var namedSourceExpression = new NamedSourceExpression(itemType, querySourceExpression, parameterExpression); - var expressions = SelectAll(itemType, parameterExpression); - var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions); - - _context.Remember(projectionExpression); + return node; } - else - { - var querySourceExpression = new QuerySourceExpression(itemType); - _context.Remember(querySourceExpression); - } - - return node; - } + Visit(node.Expression); + var source = _context.SqlExpression; - if (method == LinqMethods.QueryableSelect()) - { - // TODO: join expression - Visit(node.Arguments[0]); - var sourceItemType = GetSourceItemType(_context.SqlExpression); - var parameterExpression = new Expressions.ParameterExpression(sourceItemType, _context.NextLambdaParameterName()); - var unnamedSource = _context.SqlExpression is QuerySourceExpression - ? _context.SqlExpression - : new ParenthesesExpression(_context.SqlExpression); - var source = new NamedSourceExpression(sourceItemType, unnamedSource, parameterExpression); - - IReadOnlyCollection expressions; - - using (_context.OpenParameterScope(source.Parameter)) + ISqlExpression expression = source switch { - Visit(node.Arguments[1]); - expressions = _context.SqlExpression is Expressions.NewExpression newExpression - ? newExpression.Parameters - : new[] { _context.SqlExpression }; - } + 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) + }; - var projectionExpression = new ProjectionExpression(itemType, source, expressions); - _context.Remember(projectionExpression); + _context.Remember(expression); return node; } - if (method == LinqMethods.QueryableWhere() - || method == LinqMethods.RepositoryUpdateWhere() - || method == LinqMethods.RepositoryDeleteWhere()) + protected override Expression VisitNew(NewExpression node) { - // TODO: join expression - Visit(node.Arguments[0]); - var source = _context.SqlExpression; - - var sourceItemType = GetSourceItemType(source); - var sourceSource = GetSource(source); - var sourceSourceItemType = sourceSource != null - ? GetSourceItemType(sourceSource) - : null; + var parameters = new List(node.Arguments.Count); - switch (source) + foreach (var (memberInfo, argument) in node.Members.Zip(node.Arguments, (memberInfo, argument) => (memberInfo, argument))) { - case ProjectionExpression sourceProjectionExpression when sourceSourceItemType != null && sourceItemType != sourceSourceItemType: + if (argument is MemberExpression memberExpression + && memberExpression.Member.MemberType == MemberTypes.Property + && memberExpression.Member.Name.Equals(memberInfo.Name, StringComparison.OrdinalIgnoreCase)) { - /* - * select made a projection - */ - - var parameterExpression = new Expressions.ParameterExpression(itemType, _context.NextLambdaParameterName()); - - ISqlExpression predicate; - - using (_context.OpenParameterScope(parameterExpression)) - { - Visit(node.Arguments[1]); - predicate = _context.SqlExpression; - } - - /* - TODO: replace star with anonymous projection - var method = node.Method.GenericMethodDefinitionOrSelf(); - - if (method == LinqMethods.QueryableSelect()) - { - var itemType = node.Type.ExtractQueryableItemType(); - var sourceItemType = node.Arguments[0].Type.ExtractQueryableItemType(); - - if (itemType != sourceItemType - && itemType.IsPrimitive()) - { - _ = (itemType, sourceItemType); - - var propertyName = node.Arguments[1].UnwrapUnaryExpression() is LambdaExpression { Body: MemberExpression memberExpression } - ? memberExpression.Member.Name - : throw new NotSupportedException(node.Arguments[1].GetType().FullName); - var dynamicClass = new DynamicClass("qwe_asm", $"{sourceItemType.Name}_To_{itemType.Name}_By_{propertyName}") - .HasProperties(new DynamicProperty(itemType, propertyName)); - var type = _dynamicClassProvider.CreateType(dynamicClass); - var ctor = type.GetConstructor(Array.Empty()); - var arguments = new Expression[] { memberExpression }; - var property = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty); - - var newExpression = Expression.New(ctor); - var memberInitExpression = Expression.MemberInit(newExpression, Expression.Bind(property, memberExpression)); - _ = (ctor, arguments, property); - } - } - */ - - source = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); - IReadOnlyCollection expressions = itemType.IsPrimitive() - ? new ISqlExpression[] { new StarExpression() } - : SelectAll(itemType, parameterExpression); - var projectionExpression = new ProjectionExpression(itemType, source, expressions); - var filterExpression = new FilterExpression(itemType, projectionExpression, predicate); - _context.Remember(filterExpression); - - break; + Visit(argument); + var expression = _context.SqlExpression; + parameters.Add(expression); } - - case ProjectionExpression sourceProjectionExpression: + else { - /* - * attach predicate to a projection selection - */ - - var parameterExpression = sourceProjectionExpression.Source is NamedSourceExpression namedSourceExpression - ? namedSourceExpression.Parameter - : new Expressions.ParameterExpression(itemType, _context.NextLambdaParameterName()); - - ISqlExpression predicate; - - using (_context.OpenParameterScope(parameterExpression)) - { - Visit(node.Arguments[1]); - predicate = _context.SqlExpression; - } - - var filterExpression = new FilterExpression(itemType, sourceProjectionExpression, predicate); - _context.Remember(filterExpression); - - break; - } - - case QuerySourceExpression querySourceExpression: - { - /* - * filter raw table or view without projections - */ - - var parameterExpression = new Expressions.ParameterExpression(itemType, _context.NextLambdaParameterName()); - - ISqlExpression predicate; - - using (_context.OpenParameterScope(parameterExpression)) - { - Visit(node.Arguments[1]); - predicate = _context.SqlExpression; - } - - var namedSourceExpression = new NamedSourceExpression(itemType, querySourceExpression, parameterExpression); - var expressions = SelectAll(itemType, parameterExpression); - var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions); - var filterExpression = new FilterExpression(itemType, projectionExpression, predicate); - _context.Remember(filterExpression); - - break; - } - - default: - { - throw new NotSupportedException(source.GetType().FullName); + Visit(argument); + var expression = _context.SqlExpression is ColumnExpression + ? _context.SqlExpression + : new ParenthesesExpression(_context.SqlExpression); + var renameExpression = new RenameExpression(argument.Type, memberInfo.Name, expression); + parameters.Add(renameExpression); } } - return node; - } - - if (method == LinqMethods.QueryableOrderBy() - || method == LinqMethods.QueryableOrderByDescending() - || method == LinqMethods.QueryableThenBy() - || method == LinqMethods.QueryableThenByDescending()) - { - // TODO: - /*_context.WithinConditionalScope( - outer => outer is ProjectionExpression || outer is JoinExpression, - action => _context.WithoutScopeDuplication( - () => new NamedSourceExpression(itemType, _context), - action), - () => _context.WithoutScopeDuplication( - () => new OrderByExpression(itemType), - () => - { - var expressions = new Stack(); - var orderByExpressions = new Stack<(Expression, EnOrderingDirection)>(); - - Expression source = node; - - while (source is MethodCallExpression methodCallExpression) - { - var methodDefinition = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); - - if (methodDefinition == LinqMethods.QueryableOrderBy() - || methodDefinition == LinqMethods.QueryableOrderByDescending() - || methodDefinition == LinqMethods.QueryableThenBy() - || methodDefinition == LinqMethods.QueryableThenByDescending()) - { - var direction = methodDefinition == LinqMethods.QueryableOrderBy() || methodDefinition == LinqMethods.QueryableThenBy() - ? EnOrderingDirection.Asc - : EnOrderingDirection.Desc; - - expressions.Push(methodCallExpression.Arguments[1]); - orderByExpressions.Push((methodCallExpression.Arguments[1], direction)); - - source = methodCallExpression.Arguments[0]; - } - else - { - break; - } - } - - if (!TryBuildJoinExpression(_context, source, expressions, itemType)) - { - Visit(source); - } - - foreach (var (expression, orderingDirection) in orderByExpressions) - { - _context.WithinScope(new OrderByExpressionExpression(orderingDirection), () => Visit(expression)); - } - })); - - return node;*/ - } - - if (method == LinqMethods.Explain()) - { - Visit(node.Arguments[0]); - var source = _context.SqlExpression; - var analyze = (bool)((ConstantExpression)node.Arguments[1]).Value; - var explainExpression = new ExplainExpression(source, analyze); - _context.Remember(explainExpression); - - return node; - } - - if (method == LinqMethods.QueryableSingle() - || method == LinqMethods.QueryableSingleOrDefault()) - { - Visit(node.Arguments[0]); - var source = _context.SqlExpression; - var rowsFetchLimitExpression = new RowsFetchLimitExpression(source, 2); - _context.Remember(rowsFetchLimitExpression); + var newExpression = new Expressions.NewExpression(node.Type, parameters); + _context.Remember(newExpression); return node; } - if (method == LinqMethods.QueryableFirst() - || method == LinqMethods.QueryableFirstOrDefault()) + protected override Expression VisitConditional(ConditionalExpression node) { - Visit(node.Arguments[0]); - var source = _context.SqlExpression; - var rowsFetchLimitExpression = new RowsFetchLimitExpression(source, 1); - _context.Remember(rowsFetchLimitExpression); - - return node; - } - - if (method == LinqMethods.QueryableAny()) - { - /* - * count(*) > 0 as "Any" - */ - - Visit(node.Arguments[0]); - var parameterExpression = new Expressions.ParameterExpression(itemType, _context.NextLambdaParameterName()); - ISqlExpression source = new NamedSourceExpression(itemType, new ParenthesesExpression(_context.SqlExpression), parameterExpression); - var left = new Expressions.MethodCallExpression(typeof(int), nameof(Queryable.Count), null, new[] { new StarExpression() }); - var name = _context.NextCommandParameterName(); - var extractor = new Func(static _ => Expression.Constant(0, typeof(int))); - _context.CaptureCommandParameterExtractor(name, extractor); - 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 projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }); - _context.Remember(projectionExpression); - - return node; - } - - if (method == LinqMethods.QueryableAll()) - { - /* - * (count(case when then 1 else null end) = count(*)) as "All" - */ - - Visit(node.Arguments[0]); - var parameterExpression = new Expressions.ParameterExpression(itemType, _context.NextLambdaParameterName()); - var source = new NamedSourceExpression(itemType, _context.SqlExpression, parameterExpression); - - using (_context.OpenParameterScope(parameterExpression)) - { - Visit(node.Arguments[1]); - } - + Visit(node.Test); var when = _context.SqlExpression; - var name = _context.NextCommandParameterName(); - var extractor = new Func(static _ => Expression.Constant(1, typeof(int))); - _context.CaptureCommandParameterExtractor(name, extractor); - var then = new QueryParameterExpression(typeof(int), name); - var @else = new NullExpression(); - var conditionalExpression = new Expressions.ConditionalExpression(typeof(int), when, then, @else); - var left = new Expressions.MethodCallExpression(typeof(int), nameof(Queryable.Count), null, new[] { conditionalExpression }); - 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 projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }); - _context.Remember(projectionExpression); + Visit(node.IfTrue); + var then = _context.SqlExpression; + Visit(node.IfFalse); + var @else = _context.SqlExpression; + var conditionalExpression = new Expressions.ConditionalExpression(node.Type, when, then, @else); + _context.Remember(conditionalExpression); return node; } - if (method == LinqMethods.QueryableCount()) + protected override Expression VisitBinary(BinaryExpression node) { - /* - * count(*) as "Count" - */ - - Visit(node.Arguments[0]); - 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 projectionExpression = new ProjectionExpression(itemType, source, new[] { renameExpression }); - _context.Remember(projectionExpression); + Visit(node.Left); + var left = _context.SqlExpression; + Visit(node.Right); + var right = _context.SqlExpression; + var binaryExpression = new Expressions.BinaryExpression(node.Type, node.NodeType.AsBinaryOperator(), left, right); + _context.Remember(binaryExpression); return node; } - if (method == LinqMethods.QueryableContains()) + protected override Expression VisitUnary(UnaryExpression node) { - if (node.Arguments[0] is not ConstantExpression constantExpression - || constantExpression.Value is not IQueryable subQuery) + var bypassedExpressionTypes = new[] { - throw new InvalidOperationException("Unable to translate sub-query"); - } + ExpressionType.Quote, + ExpressionType.Convert, + ExpressionType.ConvertChecked + }; - Visit(node.Arguments[1]); - var left = _context.SqlExpression; - ISqlExpression right; - - using (_context.WithinPathScope(constantExpression)) + if (bypassedExpressionTypes.Contains(node.NodeType)) { - right = TranslateSubQuery(subQuery.Expression).Expression; + base.VisitUnary(node); } - - var binaryExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.Contains, left, right); - _context.Remember(binaryExpression); - - return node; - - SqlExpression TranslateSubQuery(Expression expression) + else { - return Translate( - _context.Clone(), - _modelProvider, - _preprocessor, - _unknownExpressionTranslators, - expression); + Visit(node.Operand); + var source = _context.SqlExpression; + var unaryExpression = new Expressions.UnaryExpression(node.Type, node.NodeType.AsUnaryOperator(), source); + _context.Remember(unaryExpression); } - } - - if (method == LinqMethods.QueryableDistinct()) - { - Visit(node.Arguments[0]); - var projection = (ProjectionExpression)_context.SqlExpression; - projection.IsDistinct = true; - - return node; - } - - if (method == LinqMethods.QueryableCast()) - { - Visit(node.Arguments[0]); return node; } - if (TryTranslateUnknownExpression(node)) + protected override Expression VisitParameter(ParameterExpression node) { - return node; - } - - throw new NotSupportedException($"method: {node.Method}"); - } - - protected override Expression VisitMember(MemberExpression node) - { - if (TryTranslateUnknownExpression(node)) - { - return node; - } + var parameterExpression = _context.ParameterExpression ?? new Expressions.ParameterExpression(node.Type, _context.NextLambdaParameterName()); - Visit(node.Expression); - var source = _context.SqlExpression; - var expression = new ColumnExpression(node.Member, node.Type, source); - _context.Remember(expression); - - return node; - } - - protected override Expression VisitNew(NewExpression node) - { - var parameters = new List(node.Arguments.Count); - - foreach (var (memberInfo, argument) in node.Members.Zip(node.Arguments, (memberInfo, argument) => (memberInfo, argument))) - { - if (argument is MemberExpression memberExpression - && memberExpression.Member.MemberType == MemberTypes.Property - && memberExpression.Member.Name.Equals(memberInfo.Name, StringComparison.OrdinalIgnoreCase)) + if (ExtractUpdateQueryRootExpressionVisitor.IsUpdateQuery(_expression) + || ExtractDeleteQueryRootExpressionVisitor.IsDeleteQuery(_expression)) { - Visit(argument); - var expression = _context.SqlExpression; - parameters.Add(expression); + parameterExpression.SkipInSql = true; } - else - { - Visit(argument); - var expression = _context.SqlExpression is ColumnExpression - ? _context.SqlExpression - : new ParenthesesExpression(_context.SqlExpression); - var renameExpression = new RenameExpression(argument.Type, memberInfo.Name, expression); - parameters.Add(renameExpression); - } - } - - var newExpression = new Expressions.NewExpression(node.Type, parameters); - _context.Remember(newExpression); - - return node; - } - protected override Expression VisitConditional(ConditionalExpression node) - { - Visit(node.Test); - var when = _context.SqlExpression; - Visit(node.IfTrue); - var then = _context.SqlExpression; - Visit(node.IfFalse); - var @else = _context.SqlExpression; - var conditionalExpression = new Expressions.ConditionalExpression(node.Type, when, then, @else); - _context.Remember(conditionalExpression); - - return node; - } - - protected override Expression VisitBinary(BinaryExpression node) - { - Visit(node.Left); - var left = _context.SqlExpression; - Visit(node.Right); - var right = _context.SqlExpression; - var binaryExpression = new Expressions.BinaryExpression(node.Type, node.NodeType.AsBinaryOperator(), left, right); - _context.Remember(binaryExpression); - - return node; - } - - protected override Expression VisitUnary(UnaryExpression node) - { - var bypassedExpressionTypes = new[] - { - ExpressionType.Quote, - ExpressionType.Convert, - ExpressionType.ConvertChecked - }; - - if (bypassedExpressionTypes.Contains(node.NodeType)) - { - base.VisitUnary(node); - } - else - { - Visit(node.Operand); - var source = _context.SqlExpression; - var unaryExpression = new Expressions.UnaryExpression(node.Type, node.NodeType.AsUnaryOperator(), source); - _context.Remember(unaryExpression); - } - - return node; - } + _context.Remember(parameterExpression); - protected override Expression VisitParameter(ParameterExpression node) - { - if (ExtractUpdateQueryRootExpressionVisitor.IsUpdateQuery(_expression) - || ExtractDeleteQueryRootExpressionVisitor.IsDeleteQuery(_expression)) - { return node; } - var parameterExpression = _context.ParameterExpression ?? new Expressions.ParameterExpression(node.Type, _context.NextLambdaParameterName()); - _context.Remember(parameterExpression); - - // todo; - /*ExtractParametersVisitor.TryExtractParameter(_context.Outer, node.Type, out var outerParameterExpression) - ? outerParameterExpression - : new Expressions.ParameterExpression(_context, node.Type),*/ - - return node; - } - - protected override Expression VisitLambda(Expression node) - { - Visit(node.Body); - - return node; - } - - protected override Expression VisitConstant(ConstantExpression node) - { - var name = _context.NextCommandParameterName(); - _context.CaptureCommandParameterExtractor(name, null); - var queryParameterExpression = new QueryParameterExpression(node.Type, name); - _context.Remember(queryParameterExpression); - - return node; - } - - private static ISqlExpression? GetSource(ISqlExpression source) - { - return source switch - { - FilterExpression filterExpression => GetSource(filterExpression.Source), - NamedSourceExpression namedSourceExpression => namedSourceExpression.Source, - ProjectionExpression projectionExpression => GetSource(projectionExpression.Source), - QuerySourceExpression => default, - _ => throw new NotSupportedException(source.GetType().FullName) - }; - } - - private static Type GetSourceItemType(ISqlExpression source) - { - return source switch - { - FilterExpression filterExpression => GetSourceItemType(filterExpression.Source), - ProjectionExpression projectionExpression => projectionExpression.ItemType, - QuerySourceExpression querySourceExpression => querySourceExpression.ItemType, - _ => throw new NotSupportedException(source.GetType().FullName) - }; - } - - private IReadOnlyCollection SelectAll( - Type type, - Expressions.ParameterExpression parameterExpression) - { - if (!type.IsClass - || type.IsPrimitive() - || type.IsCollection()) + protected override Expression VisitLambda(Expression node) { - throw new InvalidOperationException(nameof(SelectAll)); - } - - var columns = _modelProvider - .Columns(type) - .Values - .Where(column => !column.IsMultipleRelation); - - return columns - .Select(column => column.BuildExpression(parameterExpression)) - .ToList(); - } - - private bool TryTranslateUnknownExpression(Expression expression) - { - return _unknownExpressionTranslators - .Where(translator => translator.TryTranslate(_context, expression, this)) - .InformativeSingleOrDefault(Amb, expression) != null; - - static string Amb(Expression expression, IEnumerable infos) - { - return $"More than one translations suitable for expression: {expression}"; - } - } - - // TODO: - /*private bool TryBuildJoinExpression( - TranslationContext context, - Expression source, - IReadOnlyCollection expressions, - Type itemType) - { - var type = source.Type.ExtractQueryableItemType(); + Visit(node.Body); - var relations = expressions - .SelectMany(expression => ExtractRelations(type, expression, _modelProvider)) - .ToHashSet(); - - if (!relations.Any()) - { - return false; + return node; } - using (var recursiveEnumerable = relations.MoveNext()) + protected override Expression VisitConstant(ConstantExpression node) { - context.WithoutScopeDuplication( - () => new ProjectionExpression(itemType), - projection => - { - BuildJoinExpressionRecursive(_context, _modelProvider, recursiveEnumerable, () => Visit(source)); + var name = _context.NextCommandParameterName(); + _context.CaptureCommandParameterExtractor(name, null); + var queryParameterExpression = new QueryParameterExpression(node.Type, name); + _context.Remember(queryParameterExpression); - SelectAll(projection); - }); + return node; } - return true; - - static IReadOnlyCollection ExtractRelations( - Type type, - Expression node, - IModelProvider modelProvider) + private bool TryVisitLinqExpression(Expression expression) { - return type.IsSubclassOfOpenGeneric(typeof(IUniqueIdentified<>)) - ? ExtractRelationsExpressionVisitor.Extract(node, modelProvider) - : Array.Empty(); - } + return _visitor + ._linqExpressionVisitors + .Where(visitor => visitor.TryVisit(this, _context, expression)) + .InformativeSingleOrDefault(Amb, expression) != null; - static void BuildJoinExpressionRecursive( - TranslationContext context, - IModelProvider modelProvider, - IRecursiveEnumerable recursiveEnumerable, - Action? action) - { - if (recursiveEnumerable.TryMoveNext(out var relation)) - { - context.WithinScope( - new JoinExpression(), - () => - { - context.WithoutScopeDuplication( - () => new NamedSourceExpression(relation.Target, context), - () => context.Apply(new QuerySourceExpression(relation.Target))); - - BuildJoinExpressionRecursive(context, modelProvider, recursiveEnumerable, action); - - BuildJoinOnExpression(context, modelProvider, relation); - }); - } - else + static string Amb(Expression expression, IEnumerable infos) { - action?.Invoke(); - } - - static void BuildJoinOnExpression( - TranslationContext context, - IModelProvider modelProvider, - Relation relation) - { - var targetPrimaryKeyColumn = modelProvider.Tables[relation.Target].Columns[nameof(IUniqueIdentified.PrimaryKey)]; - - context.Apply(new Expressions.BinaryExpression( - typeof(bool), - BinaryOperator.Equal, - new ColumnExpression( - relation.Target.Column(nameof(IUniqueIdentified.PrimaryKey)).Reflected, - targetPrimaryKeyColumn.Type, - ExtractParametersVisitor.ExtractParameter(context.Outer, relation.Target)), - new ColumnExpression( - relation.Property.Reflected, - targetPrimaryKeyColumn.Type, - ExtractParametersVisitor.ExtractParameter(context.Outer, relation.Source)))); + return $"More than one translations are suitable for expression: {expression}"; } } - }*/ - - private static Func> GetDependencies( - IModelProvider modelProvider, - IReadOnlyDictionary map) - { - return entity => - { - var table = modelProvider.Tables[entity.GetType()]; - - return table - .Columns - .Values - .Where(column => column.IsRelation) - .Select(DependencySelector(table, entity, map)) - .Where(dependency => dependency != null) - .Select(dependency => dependency!); - - static Func DependencySelector( - ITableInfo table, - IUniqueIdentified entity, - IReadOnlyDictionary map) - { - return column => table.IsMtmTable - ? map[new EntityKey(column.Relation.Target, column.GetValue(entity) !)] - : column.GetRelationValue(entity); - } - }; - } - - private static IReadOnlyDictionary GetInsertValues( - IModelProvider modelProvider, - IReadOnlyCollection entities) - { - var map = entities - .SelectMany(modelProvider.Flatten) - .Distinct(new UniqueIdentifiedEqualityComparer()) - .ToDictionary(entity => new EntityKey(entity), entity => entity); - - return map - .Values - .OrderByDependencies(entity => new EntityKey(entity), GetDependencies(modelProvider, map)) - .SelectMany(entity => modelProvider - .Tables[entity.GetType()] - .Columns - .Values - .Where(column => !column.IsMultipleRelation) - .Select(column => - { - var value = column.GetValue(entity); - - return column.IsJsonColumn - ? Expression.Constant(new DatabaseJsonObject(value, column.Type), typeof(DatabaseJsonObject)) - : Expression.Constant(value, column.Type); - })) - .Select((expression, index) => (name: TranslationContext.CommandParameterFormat.Format(index), expression)) - .ToDictionary( - pair => pair.name, - pair => pair.expression, - StringComparer.OrdinalIgnoreCase); } } } \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/UpdateLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/UpdateLinqExpressionVisitor.cs new file mode 100644 index 00000000..3d325a02 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/UpdateLinqExpressionVisitor.cs @@ -0,0 +1,41 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + + [Component(EnLifestyle.Singleton)] + internal class UpdateLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.RepositoryUpdate()) + { + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + var updateExpression = new UpdateExpression(itemType, new List(), null); + context.Remember(updateExpression); + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs b/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs new file mode 100644 index 00000000..78a9c485 --- /dev/null +++ b/src/Modules/DataAccess.Orm.Sql/Translation/WhereLinqExpressionVisitor.cs @@ -0,0 +1,331 @@ +namespace SpaceEngineers.Core.DataAccess.Orm.Sql.Translation +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Linq.Expressions; + using AutoRegistration.Api.Abstractions; + using AutoRegistration.Api.Attributes; + using AutoRegistration.Api.Enumerations; + using Basics; + using Expressions; + using Linq; + using Model; + + [Component(EnLifestyle.Singleton)] + internal class WhereLinqExpressionVisitor : ILinqExpressionVisitor, + ICollectionResolvable + { + private readonly IModelProvider _modelProvider; + + public WhereLinqExpressionVisitor(IModelProvider modelProvider) + { + _modelProvider = modelProvider; + } + + public bool TryVisit( + ExpressionVisitor visitor, + TranslationContext context, + Expression expression) + { + if (expression is not System.Linq.Expressions.MethodCallExpression methodCallExpression) + { + return false; + } + + var method = methodCallExpression.Method.GenericMethodDefinitionOrSelf(); + + if (method == LinqMethods.QueryableWhere() + || method == LinqMethods.RepositoryUpdateWhere() + || method == LinqMethods.RepositoryDeleteWhere()) + { + var itemType = methodCallExpression.Type.ExtractQueryableItemType(); + + visitor.Visit(methodCallExpression.Arguments[0]); + var source = context.SqlExpression; + + var sourceItemType = SelectLinqExpressionVisitor.GetSourceItemType(source); + var sourceSource = SelectLinqExpressionVisitor.GetSource(source); + var sourceSourceItemType = sourceSource != null + ? SelectLinqExpressionVisitor.GetSourceItemType(sourceSource) + : null; + + switch (source) + { + case ProjectionExpression sourceProjectionExpression when sourceSourceItemType != null && sourceItemType != sourceSourceItemType: + { + /* + * 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 + */ + + var namedSourceExpression = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); + var sourceProjectionSelector = sourceProjectionExpression.Expressions.Single(); + var replacedSourceProjectionSelector = ReplaceParameterSqlExpressionVisitor.Replace(sourceProjectionSelector, parameterExpression); + var expressions = new[] { replacedSourceProjectionSelector }; + var replacedPredicateExpression = ReplaceParameterSqlExpressionVisitor.Replace(predicateExpression, replacedSourceProjectionSelector); + var filterExpression = new FilterExpression(itemType, replacedPredicateExpression); + var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); + + if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + { + projectionExpression = joinProjectionExpression; + } + + context.Remember(projectionExpression); + } + else + { + var namedSourceExpression = new NamedSourceExpression(itemType, new ParenthesesExpression(sourceProjectionExpression), parameterExpression); + var expressions = RepositoryAllLinqExpressionVisitor.SelectAll(_modelProvider, itemType, parameterExpression); + var filterExpression = new FilterExpression(itemType, predicateExpression); + var projectionExpression = new ProjectionExpression(itemType, namedSourceExpression, expressions, filterExpression, null); + + if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + { + projectionExpression = joinProjectionExpression; + } + + context.Remember(projectionExpression); + } + + break; + } + + case ProjectionExpression { Source: NamedSourceExpression namedSourceExpression } sourceProjectionExpression: + { + /* + * attach predicate to a projection selection + */ + + var parameterExpression = namedSourceExpression.Parameter; + + ISqlExpression predicateExpression; + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + + if (sourceProjectionExpression.FilterExpression is { } sourceFilterExpression) + { + predicateExpression = new Expressions.BinaryExpression(typeof(bool), BinaryOperator.AndAlso, sourceFilterExpression.Predicate, predicateExpression); + } + + var filterExpression = new FilterExpression(itemType, predicateExpression); + sourceProjectionExpression.FilterExpression = filterExpression; + context.Remember(sourceProjectionExpression); + + break; + } + + case ProjectionExpression { Source: JoinExpression joinExpression } sourceProjectionExpression: + { + /* + * apply filter to joined source + */ + + if (!TryGetJoinExpressionParameter(joinExpression, itemType, out var parameterExpression)) + { + throw new InvalidOperationException("Unable to find suitable parameter expression"); + } + + ISqlExpression predicateExpression; + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + + // TODO: replace parameter + var filterExpression = new FilterExpression(itemType, predicateExpression); + sourceProjectionExpression.FilterExpression = filterExpression; + context.Remember(sourceProjectionExpression); + + 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: + { + /* + * filter raw table or view without projections + */ + + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + + ISqlExpression predicateExpression; + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + + 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); + + if (SelectLinqExpressionVisitor.BuildJoinExpression(context, _modelProvider, projectionExpression) is { } joinProjectionExpression) + { + projectionExpression = joinProjectionExpression; + } + + context.Remember(projectionExpression); + + break; + } + + case DeleteExpression deleteExpression: + { + /* + * filter delete expression + */ + + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + + ISqlExpression predicateExpression; + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + + var filterExpression = new FilterExpression(itemType, predicateExpression); + deleteExpression.FilterExpression = filterExpression; + context.Remember(deleteExpression); + + break; + } + + case UpdateExpression updateExpression: + { + /* + * filter update expression + */ + + var parameterExpression = new Expressions.ParameterExpression(itemType, context.NextLambdaParameterName()); + + ISqlExpression predicateExpression; + + using (context.OpenParameterScope(parameterExpression)) + { + visitor.Visit(methodCallExpression.Arguments[1]); + predicateExpression = context.SqlExpression; + } + + var filterExpression = new FilterExpression(itemType, predicateExpression); + updateExpression.FilterExpression = filterExpression; + context.Remember(updateExpression); + + break; + } + + default: + { + throw new NotSupportedException(source.GetType().FullName); + } + } + + return true; + } + + return false; + } + + // TODO: private + internal class ReplaceParameterSqlExpressionVisitor + { + internal static ISqlExpression Replace(ISqlExpression expression, ISqlExpression replacement) + { + return expression switch + { + Expressions.BinaryExpression binaryExpression => new Expressions.BinaryExpression( + binaryExpression.Type, + binaryExpression.Operator, + Replace(binaryExpression.Left, replacement), + Replace(binaryExpression.Right, replacement)), + ColumnsChainExpression columnsChainExpression => new ColumnsChainExpression( + columnsChainExpression.Type, + columnsChainExpression.Members, + Replace(columnsChainExpression.Source, replacement)), + ColumnExpression columnExpression => new ColumnExpression( + columnExpression.Type, + columnExpression.Member, + Replace(columnExpression.Source, replacement)), + Expressions.ConditionalExpression conditionalExpression => new Expressions.ConditionalExpression( + conditionalExpression.Type, + Replace(conditionalExpression.When, replacement), + Replace(conditionalExpression.Then, replacement), + Replace(conditionalExpression.Else, replacement)), + JsonAttributeExpression jsonAttributeExpression => new JsonAttributeExpression( + jsonAttributeExpression.Type, + Replace(jsonAttributeExpression.Source, replacement), + Replace(jsonAttributeExpression.Accessor, replacement)), + Expressions.MethodCallExpression methodCallExpression => new Expressions.MethodCallExpression( + methodCallExpression.Type, + methodCallExpression.Name, + methodCallExpression.Source != null ? Replace(methodCallExpression.Source, replacement) : null, + methodCallExpression.Arguments.Select(argument => Replace(argument, replacement)).ToList()), + NullExpression nullExpression => nullExpression, + Expressions.ParameterExpression => replacement, + ParenthesesExpression parenthesesExpression => new ParenthesesExpression(Replace(parenthesesExpression.Source, replacement)), + QueryParameterExpression queryParameterExpression => queryParameterExpression, + Expressions.UnaryExpression unaryExpression => new Expressions.UnaryExpression( + unaryExpression.Type, + unaryExpression.Operator, + Replace(unaryExpression.Source, replacement)), + _ => throw new NotSupportedException(expression.GetType().Name) + }; + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/GenericHost.Test/LinqToSqlTests.cs b/tests/Tests/GenericHost.Test/LinqToSqlTests.cs index 82a1b811..b2f4935a 100644 --- a/tests/Tests/GenericHost.Test/LinqToSqlTests.cs +++ b/tests/Tests/GenericHost.Test/LinqToSqlTests.cs @@ -376,7 +376,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => it.NullableStringField).Where(it => it != null)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", + $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{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 @param_0 IS NULL THEN b.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE b.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)) }, log)), new IDatabaseEntity[] { databaseEntity } @@ -387,7 +387,7 @@ internal static IEnumerable CommandTranslationTestCases() new Func(container => container.Resolve().All().Select(it => it.NullableStringField).Where(it => null != it)), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", + $@"SELECT{Environment.NewLine}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{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 @param_0 IS NULL THEN b.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE b.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END", new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)) }, 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'}d.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a) b{Environment.NewLine}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}CASE WHEN @param_0 IS NULL THEN b.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE b.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END) c{Environment.NewLine}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}c.""{nameof(DatabaseEntity.IntField)}"" > @param_2 AND c.""{nameof(DatabaseEntity.IntField)}"" <= @param_3) d", + $@"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", 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 } @@ -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'}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)}""", + $@"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)}""", Array.Empty(), log)), new IDatabaseEntity[] { databaseEntity } @@ -1127,7 +1127,7 @@ internal static IEnumerable CommandTranslationTestCases() }), new Action( (query, log) => CheckSqlCommand(query, - $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END AND a.""{nameof(DatabaseEntity.PrimaryKey)}"" = ANY(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"" = @param_2) b)", + $@"SELECT{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}FROM{Environment.NewLine}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}WHERE{Environment.NewLine}{'\t'}CASE WHEN @param_0 IS NULL THEN a.""{nameof(DatabaseEntity.NullableStringField)}"" IS NOT NULL ELSE a.""{nameof(DatabaseEntity.NullableStringField)}"" != @param_1 END AND a.""{nameof(DatabaseEntity.PrimaryKey)}"" = ANY(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}b.""{nameof(DatabaseEntity.PrimaryKey)}""{Environment.NewLine}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}(SELECT{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Enum)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.IntField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.NullableStringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.PrimaryKey)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.StringField)}"",{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.Version)}""{Environment.NewLine}{'\t'}{'\t'}{'\t'}FROM{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}""{schema}"".""{nameof(DatabaseEntity)}"" a{Environment.NewLine}{'\t'}{'\t'}{'\t'}WHERE{Environment.NewLine}{'\t'}{'\t'}{'\t'}{'\t'}a.""{nameof(DatabaseEntity.BooleanField)}"" = @param_2) b)", new[] { new SqlCommandParameter("param_0", default(string), typeof(string)), new SqlCommandParameter("param_1", default(string), typeof(string)), new SqlCommandParameter("param_2", true, typeof(bool)) }, log)), new IDatabaseEntity[] { databaseEntity }