diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index 3bfe32f3bab..cfbc0526d29 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.EntityFrameworkCore.Query.Internal; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore; @@ -260,6 +261,27 @@ public static int ExecuteSqlRaw( } } + /// + /// . + /// + /// . + /// . + /// . + /// . + public static IQueryable SqlQuery( + this DatabaseFacade databaseFacade, + [NotParameterized] string sql, + params object[] parameters) + { + Check.NotNull(sql, nameof(sql)); + Check.NotNull(parameters, nameof(parameters)); + + var facadeDependencies = GetFacadeDependencies(databaseFacade); + + return facadeDependencies.QueryProvider + .CreateQuery(new SqlQueryRootExpression(typeof(TResult), sql, Expression.Constant(parameters))); + } + /// /// Executes the given SQL against the database and returns the number of rows affected. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 662861998b6..253479b2d92 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -771,12 +771,6 @@ public static string FromSqlMissingColumn(object? column) GetString("FromSqlMissingColumn", nameof(column)), column); - /// - /// 'FromSqlRaw' or 'FromSqlInterpolated' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side. - /// - public static string FromSqlNonComposable - => GetString("FromSqlNonComposable"); - /// /// The property '{propertySpecification}' has specific configuration for the function '{function}', but it isn't mapped to a column on that function return. Remove the specific configuration, or map an entity type that contains this property to '{function}'. /// @@ -895,14 +889,6 @@ public static string InvalidDerivedTypeInEntityProjection(object? derivedType, o GetString("InvalidDerivedTypeInEntityProjection", nameof(derivedType), nameof(entityType)), derivedType, entityType); - /// - /// A FromSqlExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'. - /// - public static string InvalidFromSqlArguments(object? expressionType, object? valueType) - => string.Format( - GetString("InvalidFromSqlArguments", nameof(expressionType), nameof(valueType)), - expressionType, valueType); - /// /// The grouping key '{keySelector}' is of type '{keyType}' which is not valid key. /// @@ -967,6 +953,114 @@ public static string InvalidMinBatchSize(object? value) GetString("InvalidMinBatchSize", nameof(value)), value); + /// + /// A RawSqlQuerylExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'. + /// + public static string InvalidRawSqlQueryArguments(object? expressionType, object? valueType) + => string.Format( + GetString("InvalidRawSqlQueryArguments", nameof(expressionType), nameof(valueType)), + expressionType, valueType); + + /// + /// Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. + /// + public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType, object? viewName, object? ownerType, object? ownerViewName) + => string.Format( + GetString("JsonEntityMappedToDifferentViewThanOwner", nameof(jsonType), nameof(viewName), nameof(ownerType), nameof(ownerViewName)), + jsonType, viewName, ownerType, ownerViewName); + + /// + /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. + /// + public static string JsonEntityMultipleRootsMappedToTheSameJsonColumn(object? column, object? table) + => string.Format( + GetString("JsonEntityMultipleRootsMappedToTheSameJsonColumn", nameof(column), nameof(table)), + column, table); + + /// + /// Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. + /// + public static string JsonEntityOwnedByNonJsonOwnedType(object? nonJsonType, object? table) + => string.Format( + GetString("JsonEntityOwnedByNonJsonOwnedType", nameof(nonJsonType), nameof(table)), + nonJsonType, table); + + /// + /// Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. + /// + public static string JsonEntityReferencingRegularEntity(object? jsonEntity) + => string.Format( + GetString("JsonEntityReferencingRegularEntity", nameof(jsonEntity)), + jsonEntity); + + /// + /// Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. + /// + public static string JsonEntityWithDefaultValueSetOnItsProperty(object? jsonEntity, object? property) + => string.Format( + GetString("JsonEntityWithDefaultValueSetOnItsProperty", nameof(jsonEntity), nameof(property)), + jsonEntity, property); + + /// + /// Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. + /// + public static string JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey(object? keyProperty, object? jsonEntity) + => string.Format( + GetString("JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey", nameof(keyProperty), nameof(jsonEntity)), + keyProperty, jsonEntity); + + /// + /// Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. + /// + public static string JsonEntityWithExplicitlyConfiguredOrdinalKey(object? jsonEntity) + => string.Format( + GetString("JsonEntityWithExplicitlyConfiguredOrdinalKey", nameof(jsonEntity)), + jsonEntity); + + /// + /// Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. + /// + public static string JsonEntityWithIncorrectNumberOfKeyProperties(object? jsonEntity, object? expectedCount, object? actualCount) + => string.Format( + GetString("JsonEntityWithIncorrectNumberOfKeyProperties", nameof(jsonEntity), nameof(expectedCount), nameof(actualCount)), + jsonEntity, expectedCount, actualCount); + + /// + /// Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. + /// + public static string JsonEntityWithMultiplePropertiesMappedToSameJsonProperty(object? jsonEntity, object? property) + => string.Format( + GetString("JsonEntityWithMultiplePropertiesMappedToSameJsonProperty", nameof(jsonEntity), nameof(property)), + jsonEntity, property); + + /// + /// Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. + /// + public static string JsonEntityWithNonTphInheritanceOnOwner(object? rootType, object? tph) + => string.Format( + GetString("JsonEntityWithNonTphInheritanceOnOwner", nameof(rootType), nameof(tph)), + rootType, tph); + + /// + /// Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. + /// + public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity) + => string.Format( + GetString("JsonEntityWithOwnerNotMappedToTableOrView", nameof(entity)), + entity); + + /// + /// Table splitting is not supported for entities containing entities mapped to JSON. + /// + public static string JsonEntityWithTableSplittingIsNotSupported + => GetString("JsonEntityWithTableSplittingIsNotSupported"); + + /// + /// JSON property name should only be configured on nested owned navigations. + /// + public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation + => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation"); + /// /// The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. /// @@ -997,6 +1091,12 @@ public static string MappedFunctionNotFound(object? entityType, object? function public static string MappingFragmentMissingName => GetString("MappingFragmentMissingName"); + /// + /// This method needs to be implemented in the provider. + /// + public static string MethodNeedsToBeImplementedInTheProvider + => GetString("MethodNeedsToBeImplementedInTheProvider"); + /// /// Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. /// @@ -1245,6 +1345,12 @@ public static string PropertyNotMappedToTable(object? property, object? entityTy GetString("PropertyNotMappedToTable", nameof(property), nameof(entityType), nameof(table)), property, entityType, table); + /// + /// 'FromSqlRaw' or 'FromSqlInterpolated' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side. + /// + public static string RawSqlQueryNonComposable + => GetString("RawSqlQueryNonComposable"); + /// /// The entity type '{entityType}' is not mapped to a table, therefore the entities cannot be persisted to the database. Call 'ToTable' in 'OnModelCreating' to map it to a table. /// @@ -1787,112 +1893,6 @@ public static string ViewOverrideMismatch(object? propertySpecification, object? public static string VisitChildrenMustBeOverridden => GetString("VisitChildrenMustBeOverridden"); - /// - /// Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. - /// - public static string JsonEntityOwnedByNonJsonOwnedType(object? nonJsonType, object? table) - => string.Format( - GetString("JsonEntityOwnedByNonJsonOwnedType", nameof(nonJsonType), nameof(table)), - nonJsonType, table); - - /// - /// Table splitting is not supported for entities containing entities mapped to JSON. - /// - public static string JsonEntityWithTableSplittingIsNotSupported - => GetString("JsonEntityWithTableSplittingIsNotSupported"); - - /// - /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. - /// - public static string JsonEntityMultipleRootsMappedToTheSameJsonColumn(object? column, object? table) - => string.Format( - GetString("JsonEntityMultipleRootsMappedToTheSameJsonColumn", nameof(column), nameof(table)), - column, table); - - /// - /// Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. - /// - public static string JsonEntityWithOwnerNotMappedToTableOrView(object? entity) - => string.Format( - GetString("JsonEntityWithOwnerNotMappedToTableOrView", nameof(entity)), - entity); - - /// - /// Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. - /// - public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType, object? viewName, object? ownerType, object? ownerViewName) - => string.Format( - GetString("JsonEntityMappedToDifferentViewThanOwner", nameof(jsonType), nameof(viewName), nameof(ownerType), nameof(ownerViewName)), - jsonType, viewName, ownerType, ownerViewName); - - /// - /// Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. - /// - public static string JsonEntityWithNonTphInheritanceOnOwner(object? rootType, object? tph) - => string.Format( - GetString("JsonEntityWithNonTphInheritanceOnOwner", nameof(rootType), nameof(tph)), - rootType, tph); - - /// - /// Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. - /// - public static string JsonEntityReferencingRegularEntity(object? jsonEntity) - => string.Format( - GetString("JsonEntityReferencingRegularEntity", nameof(jsonEntity)), - jsonEntity); - - /// - /// Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. - /// - public static string JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey(object? keyProperty, object? jsonEntity) - => string.Format( - GetString("JsonEntityWithExplicitlyConfiguredJsonPropertyNameOnKey", nameof(keyProperty), nameof(jsonEntity)), - keyProperty, jsonEntity); - - /// - /// Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. - /// - public static string JsonEntityWithExplicitlyConfiguredOrdinalKey(object? jsonEntity) - => string.Format( - GetString("JsonEntityWithExplicitlyConfiguredOrdinalKey", nameof(jsonEntity)), - jsonEntity); - - /// - /// Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. - /// - public static string JsonEntityWithIncorrectNumberOfKeyProperties(object? jsonEntity, object? expectedCount, object? actualCount) - => string.Format( - GetString("JsonEntityWithIncorrectNumberOfKeyProperties", nameof(jsonEntity), nameof(expectedCount), nameof(actualCount)), - jsonEntity, expectedCount, actualCount); - - /// - /// Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. - /// - public static string JsonEntityWithDefaultValueSetOnItsProperty(object? jsonEntity, object? property) - => string.Format( - GetString("JsonEntityWithDefaultValueSetOnItsProperty", nameof(jsonEntity), nameof(property)), - jsonEntity, property); - - /// - /// Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. - /// - public static string JsonEntityWithMultiplePropertiesMappedToSameJsonProperty(object? jsonEntity, object? property) - => string.Format( - GetString("JsonEntityWithMultiplePropertiesMappedToSameJsonProperty", nameof(jsonEntity), nameof(property)), - jsonEntity, property); - - /// - /// JSON property name should only be configured on nested owned navigations. - /// - public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation - => GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation"); - - /// - /// This method needs to be implemented in the provider. - /// - public static string MethodNeedsToBeImplementedInTheProvider - => GetString("MethodNeedsToBeImplementedInTheProvider"); - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index aab279ce662..369f6daae22 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -403,9 +403,6 @@ The required column '{column}' was not present in the results of a 'FromSql' operation. - - 'FromSqlRaw' or 'FromSqlInterpolated' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side. - The property '{propertySpecification}' has specific configuration for the function '{function}', but it isn't mapped to a column on that function return. Remove the specific configuration, or map an entity type that contains this property to '{function}'. @@ -451,9 +448,6 @@ The specified entity type '{derivedType}' is not derived from '{entityType}'. - - A FromSqlExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'. - The grouping key '{keySelector}' is of type '{keyType}' which is not valid key. @@ -478,6 +472,48 @@ The specified 'MinBatchSize' value '{value}' is not valid. It must be a positive number. + + A RawSqlQuerylExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'. + + + Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. + + + Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. + + + Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. + + + Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. + + + Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. + + + Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. + + + Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. + + + Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. + + + Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. + + + Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. + + + Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. + + + Table splitting is not supported for entities containing entities mapped to JSON. + + + JSON property name should only be configured on nested owned navigations. + The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. @@ -774,6 +810,9 @@ Table name must be specified to configure a table-specific property mapping. + + This method needs to be implemented in the provider. + Using '{methodName}' on DbSet of '{entityType}' is not supported since '{entityType}' is part of hierarchy and does not contain a discriminator property. @@ -876,6 +915,9 @@ The property '{property}' on entity type '{entityType}' is not mapped to '{table}'. + + 'FromSqlRaw' or 'FromSqlInterpolated' or 'SqlQuery' was called with non-composable SQL and with a query composing over it. Consider calling 'AsEnumerable' after the method to perform the composition on the client side. + The entity type '{entityType}' is not mapped to a table, therefore the entities cannot be persisted to the database. Call 'ToTable' in 'OnModelCreating' to map it to a table. @@ -1086,46 +1128,4 @@ 'VisitChildren' must be overridden in the class deriving from 'SqlExpression'. - - Owned entity type '{nonJsonType}' is mapped to table '{table}' and contains JSON columns. This is currently not supported. All owned types containing a JSON column must be mapped to a JSON column themselves. - - - Table splitting is not supported for entities containing entities mapped to JSON. - - - Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column. - - - Entity type '{entity}' references entities mapped to JSON but is not itself mapped to a table or a view.This is not supported. - - - Entity '{jsonType}' is mapped to JSON and also mapped to a view '{viewName}', however it's owner '{ownerType}' is mapped to a different view '{ownerViewName}'. Every entity mapped to JSON must also map to the same view as it's owner. - - - Entity type '{rootType}' references entities mapped to JSON. Only '{tph}' inheritance is supported for those entities. - - - Entity type '{jsonEntity}' is mapped to JSON and has navigation to a regular entity which is not the owner. - - - Key property '{keyProperty}' on JSON-mapped entity '{jsonEntity}' should not have JSON property name configured explicitly. - - - Entity type '{jsonEntity}' is part of collection mapped to JSON and has it's ordinal key defined explicitly. Only implicitly defined ordinal keys are supported. - - - Entity type '{jsonEntity}' has incorrect number of primary key properties. Expected number is: {expectedCount}, actual number is: {actualCount}. - - - Setting default value on properties of an entity mapped to JSON is not supported. Entity: '{jsonEntity}', property: '{property}'. - - - Entity '{jsonEntity}' is mapped to JSON and it contains multiple properties or navigations which are mapped to the same JSON property '{property}'. Each property should map to a unique JSON property. - - - JSON property name should only be configured on nested owned navigations. - - - This method needs to be implemented in the provider. - \ No newline at end of file diff --git a/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs b/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs index 3956a68acef..cb391b94d84 100644 --- a/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs +++ b/src/EFCore.Relational/Query/Internal/BufferedDataReader.cs @@ -1249,7 +1249,7 @@ private void InitializeFields() if (_columns.Count > 0 && _columns.Any(e => e?.Name != null)) { - // Non-Composed FromSql + // Non-Composed RawSqlQuery var readerColumns = _fieldNameLookup.Value; _indexMap = new int[_columns.Count]; @@ -1264,7 +1264,12 @@ private void InitializeFields() if (!readerColumns.TryGetValue(column.Name!, out var ordinal)) { - throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name)); + if (_columns.Count != 1) + { + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(column.Name)); + } + + ordinal = 0; } newColumnMap[ordinal] = column; diff --git a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs index b73fa6e0e9d..817b2614048 100644 --- a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs @@ -134,7 +134,12 @@ public static int[] BuildIndexMap(IReadOnlyList columnNames, DbDataReade var columnName = columnNames[i]; if (!readerColumns.TryGetValue(columnName, out var ordinal)) { - throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); + if (columnNames.Count != 1) + { + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); + } + + ordinal = 0; } indexMap[i] = ordinal; diff --git a/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/RawSqlParameterExpandingExpressionVisitor.cs similarity index 88% rename from src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs rename to src/EFCore.Relational/Query/Internal/RawSqlParameterExpandingExpressionVisitor.cs index 78a51e11b72..d52b1298ca8 100644 --- a/src/EFCore.Relational/Query/Internal/FromSqlParameterExpandingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/RawSqlParameterExpandingExpressionVisitor.cs @@ -14,10 +14,10 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal; /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// -public class FromSqlParameterExpandingExpressionVisitor : ExpressionVisitor +public class RawSqlParameterExpandingExpressionVisitor : ExpressionVisitor { - private readonly IDictionary _visitedFromSqlExpressions - = new Dictionary(LegacyReferenceEqualityComparer.Instance); + private readonly IDictionary _visitedRawSqlExpressions + = new Dictionary(LegacyReferenceEqualityComparer.Instance); private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly IRelationalTypeMappingSource _typeMappingSource; @@ -33,7 +33,7 @@ private readonly IDictionary _visitedFromSqlExpre /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public FromSqlParameterExpandingExpressionVisitor( + public RawSqlParameterExpandingExpressionVisitor( RelationalParameterBasedSqlProcessorDependencies dependencies) { Dependencies = dependencies; @@ -61,7 +61,7 @@ public virtual Expression Expand( IReadOnlyDictionary parameterValues, out bool canCache) { - _visitedFromSqlExpressions.Clear(); + _visitedRawSqlExpressions.Clear(); _parameterNameGenerator = _parameterNameGeneratorFactory.Create(); _parametersValues = parameterValues; _canCache = true; @@ -81,17 +81,17 @@ public virtual Expression Expand( [return: NotNullIfNotNull("expression")] public override Expression? Visit(Expression? expression) { - if (expression is not FromSqlExpression fromSql) + if (expression is not RawSqlQueryExpression rawSqlQuery) { return base.Visit(expression); } - if (_visitedFromSqlExpressions.TryGetValue(fromSql, out var visitedFromSql)) + if (_visitedRawSqlExpressions.TryGetValue(rawSqlQuery, out var visitedRawSql)) { - return visitedFromSql; + return visitedRawSql; } - switch (fromSql.Arguments) + switch (rawSqlQuery.Arguments) { case ParameterExpression parameterExpression: // parameter value will never be null. It could be empty object?[] @@ -127,7 +127,7 @@ public virtual Expression Expand( } } - return _visitedFromSqlExpressions[fromSql] = fromSql.Update( + return _visitedRawSqlExpressions[rawSqlQuery] = rawSqlQuery.Update( Expression.Constant(new CompositeRelationalParameter(parameterExpression.Name!, subParameters))); case ConstantExpression constantExpression: @@ -157,10 +157,10 @@ public virtual Expression Expand( } } - return _visitedFromSqlExpressions[fromSql] = fromSql.Update(Expression.Constant(constantValues, typeof(object[]))); + return _visitedRawSqlExpressions[rawSqlQuery] = rawSqlQuery.Update(Expression.Constant(constantValues, typeof(object[]))); default: - Check.DebugFail("FromSql.Arguments must be Constant/ParameterExpression"); + Check.DebugFail("RawSqlQueryExpression.Arguments must be Constant/ParameterExpression"); return null; } } diff --git a/src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs b/src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs new file mode 100644 index 00000000000..3a9bed45954 --- /dev/null +++ b/src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public sealed class SqlQueryRootExpression : QueryRootExpression +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlQueryRootExpression( + IAsyncQueryProvider queryProvider, + Type elementType, + string sql, + Expression argument) + : base(queryProvider, elementType) + { + Sql = sql; + Argument = argument; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlQueryRootExpression( + Type elementType, + string sql, + Expression argument) + : base(elementType) + { + Sql = sql; + Argument = argument; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public string Sql { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public Expression Argument { get; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override Expression DetachQueryProvider() + => new SqlQueryRootExpression(ElementType, Sql, Argument); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + var argument = visitor.Visit(Argument); + + return argument != Argument + ? new SqlQueryRootExpression(ElementType, Sql, argument) + : this; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append($"SqlQuery<{ElementType.ShortDisplayName()}>({Sql}, "); + expressionPrinter.Visit(Argument); + expressionPrinter.AppendLine(")"); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlQueryRootExpression sqlQueryRootExpression + && Equals(sqlQueryRootExpression)); + + private bool Equals(SqlQueryRootExpression sqlQueryRootExpression) + => base.Equals(sqlQueryRootExpression) + && Sql == sqlQueryRootExpression.Sql + && ExpressionEqualityComparer.Instance.Equals(Argument, sqlQueryRootExpression.Argument); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Sql, ExpressionEqualityComparer.Instance.GetHashCode(Argument)); +} diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 647bb0afcf1..af7f158624d 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -85,9 +85,9 @@ protected virtual void GenerateRootCommand(Expression queryExpression) case SelectExpression selectExpression: GenerateTagsHeaderComment(selectExpression); - if (selectExpression.IsNonComposedFromSql()) + if (selectExpression.IsNonComposedRawSqlQuery()) { - GenerateFromSql((FromSqlExpression)selectExpression.Tables[0]); + GenerateRawSqlQuery((RawSqlQueryExpression)selectExpression.Tables[0]); } else { @@ -383,12 +383,12 @@ protected override Expression VisitTable(TableExpression tableExpression) return tableExpression; } - private void GenerateFromSql(FromSqlExpression fromSqlExpression) + private void GenerateRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression) { - var sql = fromSqlExpression.Sql; + var sql = rawSqlQueryExpression.Sql; string[]? substitutions; - switch (fromSqlExpression.Arguments) + switch (rawSqlQueryExpression.Arguments) { case ConstantExpression { Value: CompositeRelationalParameter compositeRelationalParameter }: { @@ -426,11 +426,11 @@ private void GenerateFromSql(FromSqlExpression fromSqlExpression) default: throw new ArgumentOutOfRangeException( - nameof(fromSqlExpression), - fromSqlExpression.Arguments, - RelationalStrings.InvalidFromSqlArguments( - fromSqlExpression.Arguments.GetType(), - fromSqlExpression.Arguments is ConstantExpression constantExpression + nameof(rawSqlQueryExpression), + rawSqlQueryExpression.Arguments, + RelationalStrings.InvalidRawSqlQueryArguments( + rawSqlQueryExpression.Arguments.GetType(), + rawSqlQueryExpression.Arguments is ConstantExpression constantExpression ? constantExpression.Value?.GetType() : null)); } @@ -443,6 +443,7 @@ fromSqlExpression.Arguments is ConstantExpression constantExpression } /// + [Obsolete] protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) { _relationalCommandBuilder.AppendLine("("); @@ -451,7 +452,7 @@ protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) using (_relationalCommandBuilder.Indent()) { - GenerateFromSql(fromSqlExpression); + GenerateRawSqlQuery(fromSqlExpression); } _relationalCommandBuilder.Append(")") @@ -461,6 +462,25 @@ protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) return fromSqlExpression; } + /// + protected override Expression VisitRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression) + { + _relationalCommandBuilder.AppendLine("("); + + CheckComposableSql(rawSqlQueryExpression.Sql); + + using (_relationalCommandBuilder.Indent()) + { + GenerateRawSqlQuery(rawSqlQueryExpression); + } + + _relationalCommandBuilder.Append(")") + .Append(AliasSeparator) + .Append(_sqlGenerationHelper.DelimitIdentifier(rawSqlQueryExpression.Alias)); + + return rawSqlQueryExpression; + } + /// /// Checks whether a given SQL string is composable, i.e. can be embedded as a subquery within a /// larger SQL query. @@ -479,7 +499,7 @@ protected virtual void CheckComposableSql(string sql) var i = span.IndexOf('\n'); span = i > 0 ? span[(i + 1)..].TrimStart() - : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); + : throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable); continue; } @@ -489,7 +509,7 @@ protected virtual void CheckComposableSql(string sql) var i = span.IndexOf("*/"); span = i > 0 ? span[(i + 2)..].TrimStart() - : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); + : throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable); continue; } @@ -511,7 +531,7 @@ protected virtual void CheckComposableSqlTrimmed(ReadOnlySpan sql) ? sql["SELECT".Length..] : sql.StartsWith("WITH", StringComparison.OrdinalIgnoreCase) ? sql["WITH".Length..] - : throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); + : throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable); if (sql.Length > 0 && (char.IsWhiteSpace(sql[0]) || sql.StartsWith("--") || sql.StartsWith("/*"))) @@ -519,7 +539,7 @@ protected virtual void CheckComposableSqlTrimmed(ReadOnlySpan sql) return; } - throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); + throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable); } /// diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs index 14f598e221f..0d2ff815476 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessor.cs @@ -56,8 +56,8 @@ public virtual Expression Optimize( queryExpression = ProcessSqlNullability(queryExpression, parametersValues, out var sqlNullablityCanCache); canCache &= sqlNullablityCanCache; - queryExpression = ExpandFromSqlParameter(queryExpression, parametersValues, out var fromSqlParameterCanCache); - canCache &= fromSqlParameterCanCache; + queryExpression = ExpandRawSqlQueryParameter(queryExpression, parametersValues, out var rawSqlQueryParameterCanCache); + canCache &= rawSqlQueryParameterCanCache; return queryExpression; } @@ -83,9 +83,23 @@ protected virtual Expression ProcessSqlNullability( /// A dictionary of parameter values to use. /// A bool value indicating if the query expression can be cached. /// A processed query expression. + [Obsolete("Use ExpandRawSqlQueryParameter")] protected virtual Expression ExpandFromSqlParameter( Expression queryExpression, IReadOnlyDictionary parametersValues, out bool canCache) - => new FromSqlParameterExpandingExpressionVisitor(Dependencies).Expand(queryExpression, parametersValues, out canCache); + => new RawSqlParameterExpandingExpressionVisitor(Dependencies).Expand(queryExpression, parametersValues, out canCache); + + /// + /// Expands the parameters to inside the query expression for given parameter values. + /// + /// A query expression to optimize. + /// A dictionary of parameter values to use. + /// A bool value indicating if the query expression can be cached. + /// A processed query expression. + protected virtual Expression ExpandRawSqlQueryParameter( + Expression queryExpression, + IReadOnlyDictionary parametersValues, + out bool canCache) + => new RawSqlParameterExpandingExpressionVisitor(Dependencies).Expand(queryExpression, parametersValues, out canCache); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 82f2f74a278..76d8359828c 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; @@ -151,6 +152,29 @@ when entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression) new QueryExpressionReplacingExpressionVisitor(shapedQueryExpression.QueryExpression, clonedSelectExpression) .Visit(shapedQueryExpression.ShaperExpression)); + case SqlQueryRootExpression sqlQueryRootExpression: + var typeMapping = RelationalDependencies.TypeMappingSource.FindMapping(sqlQueryRootExpression.ElementType); + if (typeMapping == null) + { + throw new InvalidOperationException(); + } + + var selectExpression = new SelectExpression(sqlQueryRootExpression.Type, typeMapping, + new RawSqlQueryExpression("t", sqlQueryRootExpression.Sql, sqlQueryRootExpression.Argument)); + + Expression shaperExpression = new ProjectionBindingExpression( + selectExpression, new ProjectionMember(), sqlQueryRootExpression.ElementType.MakeNullable()); + + if (sqlQueryRootExpression.ElementType != shaperExpression.Type) + { + Check.DebugAssert(sqlQueryRootExpression.ElementType.MakeNullable() == shaperExpression.Type, + "expression.Type must be nullable of targetType"); + + shaperExpression = Expression.Convert(shaperExpression, sqlQueryRootExpression.ElementType); + } + + return new ShapedQueryExpression(selectExpression, shaperExpression); + default: return base.VisitExtension(extensionExpression); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs index 9fb23b65d9b..2f0679525a2 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitorDependencies.cs @@ -47,10 +47,12 @@ public sealed record RelationalQueryableMethodTranslatingExpressionVisitorDepend [EntityFrameworkInternal] public RelationalQueryableMethodTranslatingExpressionVisitorDependencies( IRelationalSqlTranslatingExpressionVisitorFactory relationalSqlTranslatingExpressionVisitorFactory, - ISqlExpressionFactory sqlExpressionFactory) + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) { RelationalSqlTranslatingExpressionVisitorFactory = relationalSqlTranslatingExpressionVisitorFactory; SqlExpressionFactory = sqlExpressionFactory; + TypeMappingSource = typeMappingSource; } /// @@ -62,4 +64,9 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies( /// The SQL expression factory. /// public ISqlExpressionFactory SqlExpressionFactory { get; init; } + + /// + /// The relational type mapping souce. + /// + public IRelationalTypeMappingSource TypeMappingSource { get; init; } } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 7c3a5ffe687..7ae2fa95af5 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -229,7 +229,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery var selectExpression = (SelectExpression)shapedQueryExpression.QueryExpression; VerifyNoClientConstant(shapedQueryExpression.ShaperExpression); - var nonComposedFromSql = selectExpression.IsNonComposedFromSql(); + var nonComposedFromSql = selectExpression.IsNonComposedRawSqlQuery(); var querySplittingBehavior = ((RelationalQueryCompilationContext)QueryCompilationContext).QuerySplittingBehavior; var splitQuery = querySplittingBehavior == QuerySplittingBehavior.SplitQuery; var collectionCount = 0; diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index 2aa4c1fe8f8..0cd81a4d8f1 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -55,7 +55,9 @@ protected override Expression VisitExtension(Expression extensionExpression) return VisitExists(existsExpression); case FromSqlExpression fromSqlExpression: +#pragma warning disable CS0618 // Type or member is obsolete return VisitFromSql(fromSqlExpression); +#pragma warning restore CS0618 // Type or member is obsolete case InExpression inExpression: return VisitIn(inExpression); @@ -81,8 +83,8 @@ protected override Expression VisitExtension(Expression extensionExpression) case ProjectionExpression projectionExpression: return VisitProjection(projectionExpression); - case TableValuedFunctionExpression tableValuedFunctionExpression: - return VisitTableValuedFunction(tableValuedFunctionExpression); + case RawSqlQueryExpression rawSqlQueryExpression: + return VisitRawSqlQuery(rawSqlQueryExpression); case RowNumberExpression rowNumberExpression: return VisitRowNumber(rowNumberExpression); @@ -114,6 +116,9 @@ protected override Expression VisitExtension(Expression extensionExpression) case TableExpression tableExpression: return VisitTable(tableExpression); + case TableValuedFunctionExpression tableValuedFunctionExpression: + return VisitTableValuedFunction(tableValuedFunctionExpression); + case UnionExpression unionExpression: return VisitUnion(unionExpression); } @@ -197,8 +202,16 @@ protected override Expression VisitExtension(Expression extensionExpression) /// /// The expression to visit. /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + [Obsolete("Use VisitRawSqlQuery")] protected abstract Expression VisitFromSql(FromSqlExpression fromSqlExpression); + /// + /// Visits the children of the raw sql query expression. + /// + /// The expression to visit. + /// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression. + protected abstract Expression VisitRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression); + /// /// Visits the children of the in expression. /// diff --git a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs index 344ff9bb7f8..683384d3abc 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/FromSqlExpression.cs @@ -1,20 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; - namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; /// /// -/// An expression that represents a subquery table source with user-provided custom SQL. +/// An expression that represents a subquery table source projecting entity with user-provided custom SQL. /// /// /// This type is typically used by database providers (and other extensions). It is generally /// not used in application code. /// /// -public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBase, ITableBasedExpression +public class FromSqlExpression : RawSqlQueryExpression, ITableBasedExpression { private readonly ITableBase _table; @@ -25,8 +23,9 @@ public class FromSqlExpression : TableExpressionBase, IClonableTableExpressionBa /// A user-provided custom SQL for the table source. /// A user-provided parameters to pass to the custom SQL. public FromSqlExpression(ITableBase defaultTableBase, string sql, Expression arguments) - : this(defaultTableBase.Name[..1].ToLowerInvariant(), defaultTableBase, sql, arguments, annotations: null) + : base(defaultTableBase.Name[..1].ToLowerInvariant(), sql, arguments) { + _table = defaultTableBase; } // See issue#21660/21627 @@ -45,40 +44,18 @@ private FromSqlExpression( string sql, Expression arguments, IEnumerable? annotations) - : base(alias, annotations) + : base(alias, sql, arguments, annotations) { _table = tableBase; - Sql = sql; - Arguments = arguments; - } - - /// - /// The alias assigned to this table source. - /// - [NotNull] - public override string? Alias - { - get => base.Alias!; - internal set => base.Alias = value; } - /// - /// The user-provided custom SQL for the table source. - /// - public virtual string Sql { get; } - - /// - /// The user-provided parameters passed to the custom SQL. - /// - public virtual Expression Arguments { get; } - /// /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will /// return this expression. /// - /// The property of the result. + /// The property of the result. /// This expression if no children changed, or an expression with the updated children. - public virtual FromSqlExpression Update(Expression arguments) + public override FromSqlExpression Update(Expression arguments) => arguments != Arguments ? new FromSqlExpression(Alias, _table, Sql, arguments, GetAnnotations()) : this; @@ -87,34 +64,6 @@ public virtual FromSqlExpression Update(Expression arguments) ITableBase ITableBasedExpression.Table => _table; /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - => this; - - /// - public virtual TableExpressionBase Clone() + public override TableExpressionBase Clone() => new FromSqlExpression(Alias, _table, Sql, Arguments, GetAnnotations()); - - /// - protected override void Print(ExpressionPrinter expressionPrinter) - { - expressionPrinter.Append(Sql); - PrintAnnotations(expressionPrinter); - } - - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is FromSqlExpression fromSqlExpression - && Equals(fromSqlExpression)); - - private bool Equals(FromSqlExpression fromSqlExpression) - => base.Equals(fromSqlExpression) - && _table == fromSqlExpression._table - && Sql == fromSqlExpression.Sql - && ExpressionEqualityComparer.Instance.Equals(Arguments, fromSqlExpression.Arguments); - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Sql); } diff --git a/src/EFCore.Relational/Query/SqlExpressions/RawSqlQueryExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/RawSqlQueryExpression.cs new file mode 100644 index 00000000000..5a08f03363e --- /dev/null +++ b/src/EFCore.Relational/Query/SqlExpressions/RawSqlQueryExpression.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +/// +/// +/// An expression that represents a subquery table source with user-provided custom SQL. +/// +/// +/// This type is typically used by database providers (and other extensions). It is generally +/// not used in application code. +/// +/// +public class RawSqlQueryExpression : TableExpressionBase, IClonableTableExpressionBase +{ + /// + /// Creates a new instance of the class. + /// + /// A string alias for the table source. + /// A user-provided custom SQL for the table source. + /// A user-provided parameters to pass to the custom SQL. + public RawSqlQueryExpression(string alias, string sql, Expression arguments) + : this(alias, sql, arguments, annotations: null) + { + } + + /// + /// Creates a new instance of the class. + /// + /// A string alias for the table source. + /// A user-provided custom SQL for the table source. + /// A user-provided parameters to pass to the custom SQL. + /// A collection of annotations associated with this expression. + protected RawSqlQueryExpression( + string alias, + string sql, + Expression arguments, + IEnumerable? annotations) + : base(alias, annotations) + { + Sql = sql; + Arguments = arguments; + } + + /// + /// The alias assigned to this table source. + /// + [NotNull] + public override string? Alias + { + get => base.Alias!; + internal set => base.Alias = value; + } + + /// + /// The user-provided custom SQL for the table source. + /// + public virtual string Sql { get; } + + /// + /// The user-provided parameters passed to the custom SQL. + /// + public virtual Expression Arguments { get; } + + /// + /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will + /// return this expression. + /// + /// The property of the result. + /// This expression if no children changed, or an expression with the updated children. + public virtual RawSqlQueryExpression Update(Expression arguments) + => arguments != Arguments + ? new RawSqlQueryExpression(Alias, Sql, arguments, GetAnnotations()) + : this; + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => this; + + /// + public virtual TableExpressionBase Clone() + => new RawSqlQueryExpression(Alias, Sql, Arguments, GetAnnotations()); + + /// + protected override void Print(ExpressionPrinter expressionPrinter) + { + expressionPrinter.Append(Sql); + PrintAnnotations(expressionPrinter); + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is RawSqlQueryExpression sqlQueryExpression + && Equals(sqlQueryExpression)); + + private bool Equals(RawSqlQueryExpression sqlQueryExpression) + => base.Equals(sqlQueryExpression) + && Sql == sqlQueryExpression.Sql + && ExpressionEqualityComparer.Instance.Equals(Arguments, sqlQueryExpression.Arguments); + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Sql); +} diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 8ed2e6ad81a..4e4cd1822d3 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -24,6 +24,7 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions; public sealed partial class SelectExpression : TableExpressionBase { private const string DiscriminatorColumnAlias = "Discriminator"; + private const string SqlQuerySingleColumnAlias = "Value"; private static readonly IdentifierComparer IdentifierComparerInstance = new(); private static readonly Dictionary MirroredOperationMap = @@ -91,6 +92,18 @@ internal SelectExpression(SqlExpression? projection) } } + internal SelectExpression(Type type, RelationalTypeMapping typeMapping, RawSqlQueryExpression sqlQueryExpression) + : base(null) + { + var tableReferenceExpression = new TableReferenceExpression(this, sqlQueryExpression.Alias!); + AddTable(sqlQueryExpression, tableReferenceExpression); + + var columnExpression = new ConcreteColumnExpression( + SqlQuerySingleColumnAlias, tableReferenceExpression, type, typeMapping, type.IsNullableType()); + + _projectionMapping[new ProjectionMember()] = columnExpression; + } + internal SelectExpression(IEntityType entityType, ISqlExpressionFactory sqlExpressionFactory) : base(null) { @@ -3186,6 +3199,7 @@ EntityProjectionExpression LiftEntityProjectionFromSubquery(EntityProjectionExpr /// Checks whether this represents a which is not composed upon. /// /// A bool value indicating a non-composed . + [Obsolete("Use IsNonComposedRawSqlQuery")] public bool IsNonComposedFromSql() => Limit == null && Offset == null @@ -3202,6 +3216,26 @@ public bool IsNonComposedFromSql() && _projectionMapping.TryGetValue(new ProjectionMember(), out var mapping) && mapping.Type == typeof(Dictionary); + /// + /// Checks whether this represents a which is not composed upon. + /// + /// A bool value indicating a non-composed . + public bool IsNonComposedRawSqlQuery() + => Limit == null + && Offset == null + && !IsDistinct + && Predicate == null + && GroupBy.Count == 0 + && Having == null + && Orderings.Count == 0 + && Tables.Count == 1 + && Tables[0] is RawSqlQueryExpression rawSqlQuery + && Projection.All( + pe => pe.Expression is ColumnExpression column + && string.Equals(rawSqlQuery.Alias, column.TableAlias, StringComparison.OrdinalIgnoreCase)) + && _projectionMapping.TryGetValue(new ProjectionMember(), out var mapping) + && (mapping.Type == typeof(Dictionary) || mapping.Type == typeof(int)); + /// /// Prepares the to apply aggregate operation over it. /// diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 7531af3a256..6d9569b0dd7 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -123,8 +123,8 @@ protected virtual TableExpressionBase Visit(TableExpressionBase tableExpressionB return exceptExpression.Update(source1, source2); } - case FromSqlExpression fromSqlExpression: - return fromSqlExpression; + case RawSqlQueryExpression rawSqlQueryExpression: + return rawSqlQueryExpression; case InnerJoinExpression innerJoinExpression: { diff --git a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs index bf3db896b66..b24964ee677 100644 --- a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs +++ b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs @@ -27,7 +27,8 @@ public RelationalDatabaseFacadeDependencies( IConcurrencyDetector concurrencyDetector, IRelationalConnection relationalConnection, IRawSqlCommandBuilder rawSqlCommandBuilder, - ICoreSingletonOptions coreOptions) + ICoreSingletonOptions coreOptions, + IAsyncQueryProvider queryProvider) { TransactionManager = transactionManager; DatabaseCreator = databaseCreator; @@ -39,6 +40,7 @@ public RelationalDatabaseFacadeDependencies( RelationalConnection = relationalConnection; RawSqlCommandBuilder = rawSqlCommandBuilder; CoreOptions = coreOptions; + QueryProvider = queryProvider; } /// @@ -123,4 +125,12 @@ public RelationalDatabaseFacadeDependencies( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ICoreSingletonOptions CoreOptions { get; init; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IAsyncQueryProvider QueryProvider { get; init; } } diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index f45b7632d43..d9ca62ce581 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -207,9 +207,19 @@ protected override Expression VisitExists(ExistsExpression existsExpression) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// + [Obsolete] protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) => fromSqlExpression; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitRawSqlQuery(RawSqlQueryExpression rawSqlQueryExpression) + => rawSqlQueryExpression; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index 945262affac..391aec012dd 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -246,7 +246,7 @@ protected override void CheckComposableSqlTrimmed(ReadOnlySpan sql) if (sql.StartsWith("WITH", StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException(RelationalStrings.FromSqlNonComposable); + throw new InvalidOperationException(RelationalStrings.RawSqlQueryNonComposable); } } diff --git a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs index c9fe80f5866..224192a2a19 100644 --- a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs +++ b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs @@ -64,5 +64,11 @@ public interface IDatabaseFacadeDependencies /// /// The core options. /// - public ICoreSingletonOptions CoreOptions { get; } + ICoreSingletonOptions CoreOptions { get; } + + + /// + /// The async query provider. + /// + IAsyncQueryProvider QueryProvider { get; } } diff --git a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs index 2f5b9294afa..f59c8e84489 100644 --- a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs +++ b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs @@ -25,7 +25,8 @@ public DatabaseFacadeDependencies( IEnumerable databaseProviders, IDiagnosticsLogger commandLogger, IConcurrencyDetector concurrencyDetector, - ICoreSingletonOptions coreOptions) + ICoreSingletonOptions coreOptions, + IAsyncQueryProvider queryProvider) { TransactionManager = transactionManager; DatabaseCreator = databaseCreator; @@ -35,6 +36,7 @@ public DatabaseFacadeDependencies( CommandLogger = commandLogger; ConcurrencyDetector = concurrencyDetector; CoreOptions = coreOptions; + QueryProvider = queryProvider; } /// @@ -100,4 +102,12 @@ public DatabaseFacadeDependencies( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual ICoreSingletonOptions CoreOptions { get; init; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IAsyncQueryProvider QueryProvider { get; init; } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs index 02caac1cb2b..a962bc9048a 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs @@ -142,7 +142,7 @@ public virtual async Task From_sql_queryable_stored_procedure_projection(bool as .Select(mep => mep.TenMostExpensiveProducts); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -162,7 +162,7 @@ public virtual async Task From_sql_queryable_stored_procedure_re_projection(bool new MostExpensiveProduct { TenMostExpensiveProducts = "Foo", UnitPrice = mep.UnitPrice }); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -222,7 +222,7 @@ public virtual async Task From_sql_queryable_stored_procedure_composed(bool asyn .OrderBy(mep => mep.UnitPrice); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -264,7 +264,7 @@ public virtual async Task From_sql_queryable_stored_procedure_with_parameter_com .OrderBy(coh => coh.Total); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -305,7 +305,7 @@ public virtual async Task From_sql_queryable_stored_procedure_take(bool async) .Take(2); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -343,7 +343,7 @@ public virtual async Task From_sql_queryable_stored_procedure_min(bool async) .FromSqlRaw(TenMostExpensiveProductsSproc, GetTenMostExpensiveProductsParameters()); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.MinAsync(mep => mep.UnitPrice)) : Assert.Throws(() => query.Min(mep => mep.UnitPrice))).Message); @@ -377,7 +377,7 @@ public virtual async Task From_sql_queryable_stored_procedure_with_include_throw .Include(p => p.OrderDetails); Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -397,7 +397,7 @@ from b in context.Set() select new { a, b }; Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -440,7 +440,7 @@ from p in context.Set() select new { mep, p }; Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); @@ -481,7 +481,7 @@ from mep in context.Set() select new { mep, p }; Assert.Equal( - RelationalStrings.FromSqlNonComposable, + RelationalStrings.RawSqlQueryNonComposable, (async ? await Assert.ThrowsAsync(() => query.ToArrayAsync()) : Assert.Throws(() => query.ToArray())).Message); diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs new file mode 100644 index 00000000000..eabe64fdbda --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindSqlQueryTestBase.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + +// ReSharper disable FormatStringProblem +// ReSharper disable InconsistentNaming +// ReSharper disable ConvertToConstant.Local +// ReSharper disable AccessToDisposedClosure +namespace Microsoft.EntityFrameworkCore.Query; + +public abstract class NorthwindSqlQueryTestBase : IClassFixture + where TFixture : NorthwindQueryRelationalFixture, new() +{ + protected NorthwindSqlQueryTestBase(TFixture fixture) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + } + protected TFixture Fixture { get; } + + public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SqlQuery_over_int(bool async) + { + using var context = CreateContext(); + var query = context.Database.SqlQuery(NormalizeDelimitersInRawString(@"SELECT [ProductID] FROM [Products]")); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(77, result.Count); + } + + protected string NormalizeDelimitersInRawString(string sql) + => Fixture.TestStore.NormalizeDelimitersInRawString(sql); + + protected FormattableString NormalizeDelimitersInInterpolatedString(FormattableString sql) + => Fixture.TestStore.NormalizeDelimitersInInterpolatedString(sql); + + protected abstract DbParameter CreateDbParameter(string name, object value); + + protected NorthwindContext CreateContext() + => Fixture.CreateContext(); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs index 57beeb6f479..07367ae511a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs @@ -771,7 +771,7 @@ public override async Task FromSqlRaw_composed_with_common_table_expression(bool await Assert.ThrowsAsync(() => base.FromSqlRaw_composed_with_common_table_expression(async)); - Assert.Equal(RelationalStrings.FromSqlNonComposable, exception.Message); + Assert.Equal(RelationalStrings.RawSqlQueryNonComposable, exception.Message); } protected override DbParameter CreateDbParameter(string name, object value) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSqlQuerySqlServerTest.cs new file mode 100644 index 00000000000..19ed1342bf2 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindSqlQuerySqlServerTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlClient; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSqlQuerySqlServerTest : NorthwindSqlQueryTestBase> +{ + public NorthwindSqlQuerySqlServerTest(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQuery_over_int(bool async) + { + await base.SqlQuery_over_int(async); + + AssertSql( + @"SELECT ""ProductID"" FROM ""Products"""); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SqlParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs index 0185d15146a..3946fac4630 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindFunctionsQuerySqliteTest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.EntityFrameworkCore.TestModels.Northwind; - namespace Microsoft.EntityFrameworkCore.Query; public class NorthwindFunctionsQuerySqliteTest : NorthwindFunctionsQueryRelationalTestBase< diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSqlQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSqlQuerySqliteTest.cs new file mode 100644 index 00000000000..0bba2f8d1a6 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindSqlQuerySqliteTest.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.Sqlite; + +namespace Microsoft.EntityFrameworkCore.Query; + +public class NorthwindSqlQuerySqliteTest : NorthwindSqlQueryTestBase> +{ + public NorthwindSqlQuerySqliteTest(NorthwindQuerySqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task SqlQuery_over_int(bool async) + { + await base.SqlQuery_over_int(async); + + AssertSql( + @"SELECT ""ProductID"" FROM ""Products"""); + } + + protected override DbParameter CreateDbParameter(string name, object value) + => new SqliteParameter { ParameterName = name, Value = value }; + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}