From e311f44127a31d59988c11e357f256cc31392930 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Fri, 31 May 2019 17:46:40 -0700 Subject: [PATCH] Query: Enable parameter & sproc support for FromSql Adds optimizer to optimize SelectExpression before printing based on ParameterValues TODO: Add a factory through service to avoid multiple services here. This also serves point for us to determine second level caching Lift FromSqlExpression when not composed over to avoid subquery which is necessary for stored procedures Add additional enumerables which add remapping map to materialize non composed SelectExpression which can have out of order projections Resolves#15750 --- ...ntityFrameworkRelationalServicesBuilder.cs | 2 +- .../Query/Pipeline/AsyncQueryingEnumerable.cs | 26 ++- ...omSqlNonComposedAsyncQueryingEnumerable.cs | 177 ++++++++++++++++++ .../FromSqlNonComposedQueryingEnumerable.cs | 177 ++++++++++++++++++ ...omSqlParameterApplyingExpressionVisitor.cs | 95 ++++++++++ .../Query/Pipeline/ISqlExpressionFactory.cs | 1 + ...ressionValuesExpandingExpressionVisitor.cs | 100 ++++++++++ ...eterValueBasedSelectExpressionOptimizer.cs | 40 ++++ .../Query/Pipeline/QuerySqlGenerator.cs | 157 ++++++---------- .../Query/Pipeline/QueryingEnumerable.cs | 23 ++- ...alShapedQueryCompilingExpressionVisitor.cs | 66 +++++++ ...onalShapedQueryExpressionVisitorFactory.cs | 15 +- .../Query/Pipeline/SqlExpressionFactory.cs | 5 + .../SqlExpressions/SelectExpression.cs | 12 ++ .../Storage/RelationalCommand.cs | 2 +- .../RelationalCommandBuilderExtensions.cs | 2 +- .../Storage/RelationalSqlGenerationHelper.cs | 4 +- .../Query/AsyncFromSqlQueryTestBase.cs | 10 +- .../Query/FromSqlQueryTestBase.cs | 34 ++-- .../Query/FromSqlSprocQueryTestBase.cs | 2 +- .../AsyncFromSqlSprocQuerySqlServerTest.cs | 2 +- .../Query/FromSqlQuerySqlServerTest.cs | 8 +- .../Query/FromSqlSprocQuerySqlServerTest.cs | 4 +- .../SqlServerComplianceTest.cs | 2 - 24 files changed, 815 insertions(+), 151 deletions(-) create mode 100644 src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedAsyncQueryingEnumerable.cs create mode 100644 src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedQueryingEnumerable.cs create mode 100644 src/EFCore.Relational/Query/Pipeline/FromSqlParameterApplyingExpressionVisitor.cs create mode 100644 src/EFCore.Relational/Query/Pipeline/InExpressionValuesExpandingExpressionVisitor.cs create mode 100644 src/EFCore.Relational/Query/Pipeline/ParameterValueBasedSelectExpressionOptimizer.cs diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index d5db7a50471..21dc6295188 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -94,7 +94,7 @@ public static readonly IDictionary RelationalServi { typeof(IRelationalTypeMappingSourcePlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, // New Query Pipeline - { typeof(IQuerySqlGeneratorFactory2), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IQuerySqlGeneratorFactory2), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IRelationalSqlTranslatingExpressionVisitorFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMethodCallTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IMemberTranslatorProvider), new ServiceCharacteristics(ServiceLifetime.Singleton) }, diff --git a/src/EFCore.Relational/Query/Pipeline/AsyncQueryingEnumerable.cs b/src/EFCore.Relational/Query/Pipeline/AsyncQueryingEnumerable.cs index e5c3b80cee2..c5d18b20d33 100644 --- a/src/EFCore.Relational/Query/Pipeline/AsyncQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Pipeline/AsyncQueryingEnumerable.cs @@ -23,10 +23,14 @@ private class AsyncQueryingEnumerable : IAsyncEnumerable private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; public AsyncQueryingEnumerable( RelationalQueryContext relationalQueryContext, IQuerySqlGeneratorFactory2 querySqlGeneratorFactory, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, SelectExpression selectExpression, Func> shaper, Type contextType, @@ -34,16 +38,15 @@ public AsyncQueryingEnumerable( { _relationalQueryContext = relationalQueryContext; _querySqlGeneratorFactory = querySqlGeneratorFactory; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; _selectExpression = selectExpression; _shaper = shaper; _contextType = contextType; _logger = logger; } - public IAsyncEnumerator GetEnumerator() - { - return new AsyncEnumerator(this); - } + public IAsyncEnumerator GetEnumerator() => new AsyncEnumerator(this); private sealed class AsyncEnumerator : IAsyncEnumerator { @@ -55,6 +58,8 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; public AsyncEnumerator(AsyncQueryingEnumerable queryingEnumerable) { @@ -64,6 +69,8 @@ public AsyncEnumerator(AsyncQueryingEnumerable queryingEnumerable) _querySqlGeneratorFactory = queryingEnumerable._querySqlGeneratorFactory; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; + _sqlExpressionFactory = queryingEnumerable._sqlExpressionFactory; + _parameterNameGeneratorFactory = queryingEnumerable._parameterNameGeneratorFactory; } public T Current { get; private set; } @@ -85,11 +92,12 @@ public async Task MoveNext(CancellationToken cancellationToken) try { - var relationalCommand = _querySqlGeneratorFactory.Create() - .GetCommand( - _selectExpression, - _relationalQueryContext.ParameterValues, - _relationalQueryContext.CommandLogger); + var selectExpression = new ParameterValueBasedSelectExpressionOptimizer( + _sqlExpressionFactory, + _parameterNameGeneratorFactory) + .Optimize(_selectExpression, _relationalQueryContext.ParameterValues); + + var relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); _dataReader = await relationalCommand.ExecuteReaderAsync( diff --git a/src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedAsyncQueryingEnumerable.cs b/src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedAsyncQueryingEnumerable.cs new file mode 100644 index 00000000000..bf9bbfce221 --- /dev/null +++ b/src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedAsyncQueryingEnumerable.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline +{ + public partial class RelationalShapedQueryCompilingExpressionVisitor + { + private class FromSqlNonComposedAsyncQueryingEnumerable : IAsyncEnumerable + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly SelectExpression _selectExpression; + private readonly Func> _shaper; + private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + + public FromSqlNonComposedAsyncQueryingEnumerable( + RelationalQueryContext relationalQueryContext, + IQuerySqlGeneratorFactory2 querySqlGeneratorFactory, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, + SelectExpression selectExpression, + Func> shaper, + Type contextType, + IDiagnosticsLogger logger) + { + _relationalQueryContext = relationalQueryContext; + _querySqlGeneratorFactory = querySqlGeneratorFactory; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; + _selectExpression = selectExpression; + _shaper = shaper; + _contextType = contextType; + _logger = logger; + } + + public IAsyncEnumerator GetEnumerator() => new AsyncEnumerator(this); + + private sealed class AsyncEnumerator : IAsyncEnumerator + { + private RelationalDataReader _dataReader; + private int[] _indexMap; + private readonly RelationalQueryContext _relationalQueryContext; + private readonly SelectExpression _selectExpression; + private readonly Func> _shaper; + private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + + public AsyncEnumerator(FromSqlNonComposedAsyncQueryingEnumerable queryingEnumerable) + { + _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _shaper = queryingEnumerable._shaper; + _selectExpression = queryingEnumerable._selectExpression; + _querySqlGeneratorFactory = queryingEnumerable._querySqlGeneratorFactory; + _contextType = queryingEnumerable._contextType; + _logger = queryingEnumerable._logger; + _sqlExpressionFactory = queryingEnumerable._sqlExpressionFactory; + _parameterNameGeneratorFactory = queryingEnumerable._parameterNameGeneratorFactory; + } + + public T Current { get; private set; } + + + public void Dispose() + { + _dataReader?.Dispose(); + _dataReader = null; + _relationalQueryContext.Connection.Close(); + } + + public async Task MoveNext(CancellationToken cancellationToken) + { + try + { + if (_dataReader == null) + { + _relationalQueryContext.Connection.Open(); + + try + { + var projection = _selectExpression.Projection.ToList(); + + var selectExpression = new ParameterValueBasedSelectExpressionOptimizer( + _sqlExpressionFactory, + _parameterNameGeneratorFactory) + .Optimize(_selectExpression, _relationalQueryContext.ParameterValues); + + var relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); + + _dataReader + = await relationalCommand.ExecuteReaderAsync( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + _relationalQueryContext.CommandLogger, + cancellationToken); + + var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) + .Select( + i => new + { + Name = _dataReader.DbDataReader.GetName(i), + Ordinal = i + }).ToList(); + + _indexMap = new int[projection.Count]; + + for (var i = 0; i < projection.Count; i++) + { + if (projection[i].Expression is ColumnExpression columnExpression) + { + var columnName = columnExpression.Name; + + if (columnName != null) + { + var readerColumn + = readerColumns.SingleOrDefault( + c => + string.Equals(columnName, c.Name, StringComparison.OrdinalIgnoreCase)); + + if (readerColumn == null) + { + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); + } + + _indexMap[i] = readerColumn.Ordinal; + } + } + } + } + catch (Exception) + { + // If failure happens creating the data reader, then it won't be available to + // handle closing the connection, so do it explicitly here to preserve ref counting. + _relationalQueryContext.Connection.Close(); + + throw; + } + } + + var hasNext = await _dataReader.ReadAsync(cancellationToken); + + Current + = hasNext + ? await _shaper(_relationalQueryContext, _dataReader.DbDataReader, _indexMap) + : default; + + return hasNext; + } + catch (Exception exception) + { + _logger.QueryIterationFailed(_contextType, exception); + + throw; + } + } + + public void Reset() => throw new NotImplementedException(); + } + } + } +} diff --git a/src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedQueryingEnumerable.cs b/src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedQueryingEnumerable.cs new file mode 100644 index 00000000000..3eaab6c9fce --- /dev/null +++ b/src/EFCore.Relational/Query/Pipeline/FromSqlNonComposedQueryingEnumerable.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline +{ + public partial class RelationalShapedQueryCompilingExpressionVisitor + { + private class FromSqlNonComposedQueryingEnumerable : IEnumerable + { + private readonly RelationalQueryContext _relationalQueryContext; + private readonly SelectExpression _selectExpression; + private readonly Func _shaper; + private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + + public FromSqlNonComposedQueryingEnumerable( + RelationalQueryContext relationalQueryContext, + IQuerySqlGeneratorFactory2 querySqlGeneratorFactory, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, + SelectExpression selectExpression, + Func shaper, + Type contextType, + IDiagnosticsLogger logger) + { + _relationalQueryContext = relationalQueryContext; + _querySqlGeneratorFactory = querySqlGeneratorFactory; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; + _selectExpression = selectExpression; + _shaper = shaper; + _contextType = contextType; + _logger = logger; + } + + public IEnumerator GetEnumerator() => new Enumerator(this); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private sealed class Enumerator : IEnumerator + { + private RelationalDataReader _dataReader; + private int[] _indexMap; + private readonly RelationalQueryContext _relationalQueryContext; + private readonly SelectExpression _selectExpression; + private readonly Func _shaper; + private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; + private readonly Type _contextType; + private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + + public Enumerator(FromSqlNonComposedQueryingEnumerable queryingEnumerable) + { + _relationalQueryContext = queryingEnumerable._relationalQueryContext; + _shaper = queryingEnumerable._shaper; + _selectExpression = queryingEnumerable._selectExpression; + _querySqlGeneratorFactory = queryingEnumerable._querySqlGeneratorFactory; + _contextType = queryingEnumerable._contextType; + _logger = queryingEnumerable._logger; + _sqlExpressionFactory = queryingEnumerable._sqlExpressionFactory; + _parameterNameGeneratorFactory = queryingEnumerable._parameterNameGeneratorFactory; + } + + public T Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + _dataReader?.Dispose(); + _dataReader = null; + _relationalQueryContext.Connection.Close(); + } + + public bool MoveNext() + { + try + { + if (_dataReader == null) + { + _relationalQueryContext.Connection.Open(); + + try + { + var projection = _selectExpression.Projection.ToList(); + + var selectExpression = new ParameterValueBasedSelectExpressionOptimizer( + _sqlExpressionFactory, + _parameterNameGeneratorFactory) + .Optimize(_selectExpression, _relationalQueryContext.ParameterValues); + + var relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); + + _dataReader + = relationalCommand.ExecuteReader( + _relationalQueryContext.Connection, + _relationalQueryContext.ParameterValues, + _relationalQueryContext.CommandLogger); + + var readerColumns = Enumerable.Range(0, _dataReader.DbDataReader.FieldCount) + .Select( + i => new + { + Name = _dataReader.DbDataReader.GetName(i), + Ordinal = i + }).ToList(); + + _indexMap = new int[projection.Count]; + + for (var i = 0; i < projection.Count; i++) + { + if (projection[i].Expression is ColumnExpression columnExpression) + { + var columnName = columnExpression.Name; + + if (columnName != null) + { + var readerColumn + = readerColumns.SingleOrDefault( + c => + string.Equals(columnName, c.Name, StringComparison.OrdinalIgnoreCase)); + + if (readerColumn == null) + { + throw new InvalidOperationException(RelationalStrings.FromSqlMissingColumn(columnName)); + } + + _indexMap[i] = readerColumn.Ordinal; + } + } + } + } + catch (Exception) + { + // If failure happens creating the data reader, then it won't be available to + // handle closing the connection, so do it explicitly here to preserve ref counting. + _relationalQueryContext.Connection.Close(); + + throw; + } + } + + var hasNext = _dataReader.Read(); + + Current + = hasNext + ? _shaper(_relationalQueryContext, _dataReader.DbDataReader, _indexMap) + : default; + + return hasNext; + } + catch (Exception exception) + { + _logger.QueryIterationFailed(_contextType, exception); + + throw; + } + } + + public void Reset() => throw new NotImplementedException(); + } + } + } +} diff --git a/src/EFCore.Relational/Query/Pipeline/FromSqlParameterApplyingExpressionVisitor.cs b/src/EFCore.Relational/Query/Pipeline/FromSqlParameterApplyingExpressionVisitor.cs new file mode 100644 index 00000000000..bdcf2fe716d --- /dev/null +++ b/src/EFCore.Relational/Query/Pipeline/FromSqlParameterApplyingExpressionVisitor.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline +{ + public partial class RelationalShapedQueryCompilingExpressionVisitor + { + private class FromSqlParameterApplyingExpressionVisitor : ExpressionVisitor + { + private readonly IDictionary _visitedFromSqlExpressions + = new Dictionary(ReferenceEqualityComparer.Instance); + + private readonly ISqlExpressionFactory _SqlExpressionFactory; + private readonly ParameterNameGenerator _parameterNameGenerator; + private IReadOnlyDictionary _parametersValues; + + public FromSqlParameterApplyingExpressionVisitor( + ISqlExpressionFactory _sqlExpressionFactory, + ParameterNameGenerator parameterNameGenerator, + IReadOnlyDictionary parametersValues) + { + _SqlExpressionFactory = _sqlExpressionFactory; + _parameterNameGenerator = parameterNameGenerator; + _parametersValues = parametersValues; + } + + public override Expression Visit(Expression expression) + { + if (expression is FromSqlExpression fromSql) + { + if (!_visitedFromSqlExpressions.TryGetValue(fromSql, out var updatedFromSql)) + { + switch (fromSql.Arguments) + { + case ParameterExpression parameterExpression: + var parameterValues = (object[])_parametersValues[parameterExpression.Name]; + + var subParameters = new List(parameterValues.Length); + for (var i = 0; i < parameterValues.Length; i++) + { + var parameterName = _parameterNameGenerator.GenerateNext(); + if (parameterValues[i] is DbParameter dbParameter) + { + if (string.IsNullOrEmpty(dbParameter.ParameterName)) + { + dbParameter.ParameterName = parameterName; + } + else + { + parameterName = dbParameter.ParameterName; + } + + subParameters.Add(new RawRelationalParameter(parameterName, dbParameter)); + } + else + { + subParameters.Add( + new TypeMappedRelationalParameter( + parameterName, + parameterName, + _SqlExpressionFactory.GetTypeMappingForValue(parameterValues[i]), + parameterValues[i]?.GetType().IsNullableType())); + } + } + + updatedFromSql = new FromSqlExpression( + fromSql.Sql, + Expression.Constant( + new CompositeRelationalParameter( + parameterExpression.Name, + subParameters)), + fromSql.Alias); + + _visitedFromSqlExpressions[fromSql] = updatedFromSql; + break; + } + } + + return updatedFromSql; + } + + return base.Visit(expression); + } + } + } +} diff --git a/src/EFCore.Relational/Query/Pipeline/ISqlExpressionFactory.cs b/src/EFCore.Relational/Query/Pipeline/ISqlExpressionFactory.cs index c2d47479730..8eaeee3b3d4 100644 --- a/src/EFCore.Relational/Query/Pipeline/ISqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/Pipeline/ISqlExpressionFactory.cs @@ -15,6 +15,7 @@ public interface ISqlExpressionFactory #region TypeMapping SqlExpression ApplyTypeMapping(SqlExpression sqlExpression, RelationalTypeMapping typeMapping); SqlExpression ApplyDefaultTypeMapping(SqlExpression sqlExpression); + RelationalTypeMapping GetTypeMappingForValue(object value); #endregion #region Binary diff --git a/src/EFCore.Relational/Query/Pipeline/InExpressionValuesExpandingExpressionVisitor.cs b/src/EFCore.Relational/Query/Pipeline/InExpressionValuesExpandingExpressionVisitor.cs new file mode 100644 index 00000000000..c06cadaf33e --- /dev/null +++ b/src/EFCore.Relational/Query/Pipeline/InExpressionValuesExpandingExpressionVisitor.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline +{ + public partial class RelationalShapedQueryCompilingExpressionVisitor + { + private class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor + { + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private IReadOnlyDictionary _parametersValues; + + public InExpressionValuesExpandingExpressionVisitor( + ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary parametersValues) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parametersValues = parametersValues; + } + + public override Expression Visit(Expression expression) + { + if (expression is InExpression inExpression + && inExpression.Values != null) + { + var inValues = new List(); + var hasNullValue = false; + RelationalTypeMapping typeMapping = null; + + switch (inExpression.Values) + { + case SqlConstantExpression sqlConstant: + { + typeMapping = sqlConstant.TypeMapping; + var values = (IEnumerable)sqlConstant.Value; + foreach (var value in values) + { + if (value == null) + { + hasNullValue = true; + continue; + } + + inValues.Add(value); + } + } + break; + + case SqlParameterExpression sqlParameter: + { + typeMapping = sqlParameter.TypeMapping; + var values = (IEnumerable)_parametersValues[sqlParameter.Name]; + foreach (var value in values) + { + if (value == null) + { + hasNullValue = true; + continue; + } + + inValues.Add(value); + } + } + break; + } + + var updatedInExpression = inValues.Count > 0 + ? _sqlExpressionFactory.In( + (SqlExpression)Visit(inExpression.Item), + _sqlExpressionFactory.Constant(inValues, typeMapping), + inExpression.Negated) + : null; + + var nullCheckExpression = hasNullValue + ? _sqlExpressionFactory.IsNull(inExpression.Item) + : null; + + if (updatedInExpression != null && nullCheckExpression != null) + { + return _sqlExpressionFactory.OrElse(updatedInExpression, nullCheckExpression); + } + + if (updatedInExpression == null && nullCheckExpression == null) + { + return _sqlExpressionFactory.Equal(_sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(false)); + } + + return (SqlExpression)updatedInExpression ?? nullCheckExpression; + } + + return base.Visit(expression); + } + } + } +} diff --git a/src/EFCore.Relational/Query/Pipeline/ParameterValueBasedSelectExpressionOptimizer.cs b/src/EFCore.Relational/Query/Pipeline/ParameterValueBasedSelectExpressionOptimizer.cs new file mode 100644 index 00000000000..d967ff62f78 --- /dev/null +++ b/src/EFCore.Relational/Query/Pipeline/ParameterValueBasedSelectExpressionOptimizer.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline +{ + public partial class RelationalShapedQueryCompilingExpressionVisitor + { + private class ParameterValueBasedSelectExpressionOptimizer + { + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; + + public ParameterValueBasedSelectExpressionOptimizer( + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; + } + + public SelectExpression Optimize(SelectExpression selectExpression, IReadOnlyDictionary parametersValues) + { + var query = new InExpressionValuesExpandingExpressionVisitor( + _sqlExpressionFactory, parametersValues).Visit(selectExpression); + + query = new FromSqlParameterApplyingExpressionVisitor( + _sqlExpressionFactory, + _parameterNameGeneratorFactory.Create(), + parametersValues).Visit(query); + + + return (SelectExpression)query; + } + } + } +} diff --git a/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs index ef8c0a50dfa..0e731d20853 100644 --- a/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/Pipeline/QuerySqlGenerator.cs @@ -4,11 +4,13 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline { @@ -17,8 +19,6 @@ public class QuerySqlGenerator : SqlExpressionVisitor private readonly IRelationalCommandBuilderFactory _relationalCommandBuilderFactory; private readonly ISqlGenerationHelper _sqlGenerationHelper; private IRelationalCommandBuilder _relationalCommandBuilder; - private IReadOnlyDictionary _parametersValues; - //private ParameterNameGenerator _parameterNameGenerator; private static readonly Dictionary _operatorMap = new Dictionary { @@ -39,25 +39,26 @@ public class QuerySqlGenerator : SqlExpressionVisitor { ExpressionType.Or, " | " } }; - public QuerySqlGenerator(IRelationalCommandBuilderFactory relationalCommandBuilderFactory, + public QuerySqlGenerator( + IRelationalCommandBuilderFactory relationalCommandBuilderFactory, ISqlGenerationHelper sqlGenerationHelper) { _relationalCommandBuilderFactory = relationalCommandBuilderFactory; _sqlGenerationHelper = sqlGenerationHelper; } - public virtual IRelationalCommand GetCommand( - SelectExpression selectExpression, - IReadOnlyDictionary parameterValues, - IDiagnosticsLogger commandLogger) + public virtual IRelationalCommand GetCommand(SelectExpression selectExpression) { _relationalCommandBuilder = _relationalCommandBuilderFactory.Create(); - //_parameterNameGenerator = Dependencies.ParameterNameGeneratorFactory.Create(); - - _parametersValues = parameterValues; - - VisitSelect(selectExpression); + if (selectExpression.IsNonComposedFromSql()) + { + GenerateFromSql((FromSqlExpression)selectExpression.Tables[0]); + } + else + { + VisitSelect(selectExpression); + } return _relationalCommandBuilder.Build(); } @@ -101,16 +102,14 @@ protected override Expression VisitSelect(SelectExpression selectExpression) if (selectExpression.Tables.Any()) { - _relationalCommandBuilder.AppendLine() - .Append("FROM "); + _relationalCommandBuilder.AppendLine().Append("FROM "); GenerateList(selectExpression.Tables, e => Visit(e), sql => sql.AppendLine()); } if (selectExpression.Predicate != null) { - _relationalCommandBuilder.AppendLine() - .Append("WHERE "); + _relationalCommandBuilder.AppendLine().Append("WHERE "); Visit(selectExpression.Predicate); } @@ -135,8 +134,7 @@ protected override Expression VisitSelect(SelectExpression selectExpression) } else if (selectExpression.Offset != null) { - _relationalCommandBuilder.AppendLine() - .Append("ORDER BY (SELECT 1)"); + _relationalCommandBuilder.AppendLine().Append("ORDER BY (SELECT 1)"); } GenerateLimitOffset(selectExpression); @@ -216,14 +214,47 @@ protected override Expression VisitTable(TableExpression tableExpression) return tableExpression; } + private void GenerateFromSql(FromSqlExpression fromSqlExpression) + { + var sql = fromSqlExpression.Sql; + string[] substitutions = null; + + switch (fromSqlExpression.Arguments) + { + case ConstantExpression constantExpression + when constantExpression.Value is CompositeRelationalParameter compositeRelationalParameter: + { + var subParameters = compositeRelationalParameter.RelationalParameters; + substitutions = new string[subParameters.Count]; + for (var i = 0; i < subParameters.Count; i++) + { + substitutions[i] = _sqlGenerationHelper.GenerateParameterNamePlaceholder(subParameters[i].InvariantName); + } + + _relationalCommandBuilder.AddParameter(compositeRelationalParameter); + + break; + } + } + + if (substitutions != null) + { + // ReSharper disable once CoVariantArrayConversion + // InvariantCulture not needed since substitutions are all strings + sql = string.Format(sql, substitutions); + } + + + _relationalCommandBuilder.AppendLines(sql); + } + protected override Expression VisitFromSql(FromSqlExpression fromSqlExpression) { _relationalCommandBuilder.AppendLine("("); using (_relationalCommandBuilder.Indent()) { - _relationalCommandBuilder.AppendLines(fromSqlExpression.Sql); - // TODO: Generate parameters + GenerateFromSql(fromSqlExpression); } _relationalCommandBuilder.Append(") AS ") @@ -470,84 +501,14 @@ protected override Expression VisitIn(InExpression inExpression) { if (inExpression.Values != null) { - var inValues = new List(); - var hasNullValue = false; - - switch (inExpression.Values) - { - case SqlConstantExpression sqlConstant: - { - var values = (IEnumerable)sqlConstant.Value; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add( - new SqlConstantExpression( - Expression.Constant(value), - sqlConstant.TypeMapping)); - } - } - break; - - case SqlParameterExpression sqlParameter: - { - var values = (IEnumerable)_parametersValues[sqlParameter.Name]; - foreach (var value in values) - { - if (value == null) - { - hasNullValue = true; - continue; - } - - inValues.Add( - new SqlConstantExpression( - Expression.Constant(value), - sqlParameter.TypeMapping)); - } - } - break; - } - - if (inValues.Count > 0) - { - if (hasNullValue) - { - _relationalCommandBuilder.Append("("); - } - - Visit(inExpression.Item); - _relationalCommandBuilder.Append(inExpression.Negated ? " NOT IN " : " IN "); - _relationalCommandBuilder.Append("("); - GenerateList(inValues, e => Visit(e)); - _relationalCommandBuilder.Append(")"); - - if (hasNullValue) - { - _relationalCommandBuilder.Append(_operatorMap[ExpressionType.OrElse]); - Visit(new SqlUnaryExpression( - ExpressionType.Equal, inExpression.Item, inExpression.Type, inExpression.TypeMapping)); - _relationalCommandBuilder.Append(")"); - } - } - else - { - Visit( - hasNullValue - ? (Expression)new SqlUnaryExpression( - ExpressionType.Equal, inExpression.Item, inExpression.Type, inExpression.TypeMapping) - : new SqlBinaryExpression( - ExpressionType.Equal, - new SqlConstantExpression(Expression.Constant(true), inExpression.TypeMapping), - new SqlConstantExpression(Expression.Constant(false), inExpression.TypeMapping), - typeof(bool), - inExpression.TypeMapping)); - } + Visit(inExpression.Item); + _relationalCommandBuilder.Append(inExpression.Negated ? " NOT IN " : " IN "); + _relationalCommandBuilder.Append("("); + var valuesConstant = (SqlConstantExpression)inExpression.Values; + var valuesList = ((IEnumerable)valuesConstant.Value) + .Select(v => new SqlConstantExpression(Expression.Constant(v), valuesConstant.TypeMapping)).ToList(); + GenerateList(valuesList, e => Visit(e)); + _relationalCommandBuilder.Append(")"); } else { diff --git a/src/EFCore.Relational/Query/Pipeline/QueryingEnumerable.cs b/src/EFCore.Relational/Query/Pipeline/QueryingEnumerable.cs index a9537ed8328..b4c7fb67474 100644 --- a/src/EFCore.Relational/Query/Pipeline/QueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Pipeline/QueryingEnumerable.cs @@ -5,6 +5,8 @@ using System.Collections; using System.Collections.Generic; using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; @@ -22,9 +24,13 @@ private class QueryingEnumerable : IEnumerable private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; public QueryingEnumerable(RelationalQueryContext relationalQueryContext, IQuerySqlGeneratorFactory2 querySqlGeneratorFactory, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, SelectExpression selectExpression, Func shaper, Type contextType, @@ -32,6 +38,8 @@ public QueryingEnumerable(RelationalQueryContext relationalQueryContext, { _relationalQueryContext = relationalQueryContext; _querySqlGeneratorFactory = querySqlGeneratorFactory; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; _selectExpression = selectExpression; _shaper = shaper; _contextType = contextType; @@ -51,6 +59,8 @@ private sealed class Enumerator : IEnumerator private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; public Enumerator(QueryingEnumerable queryingEnumerable) { @@ -60,6 +70,8 @@ public Enumerator(QueryingEnumerable queryingEnumerable) _querySqlGeneratorFactory = queryingEnumerable._querySqlGeneratorFactory; _contextType = queryingEnumerable._contextType; _logger = queryingEnumerable._logger; + _sqlExpressionFactory = queryingEnumerable._sqlExpressionFactory; + _parameterNameGeneratorFactory = queryingEnumerable._parameterNameGeneratorFactory; } public T Current { get; private set; } @@ -83,11 +95,12 @@ public bool MoveNext() try { - var relationalCommand = _querySqlGeneratorFactory.Create() - .GetCommand( - _selectExpression, - _relationalQueryContext.ParameterValues, - _relationalQueryContext.CommandLogger); + var selectExpression = new ParameterValueBasedSelectExpressionOptimizer( + _sqlExpressionFactory, + _parameterNameGeneratorFactory) + .Optimize(_selectExpression, _relationalQueryContext.ParameterValues); + + var relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression); _dataReader = relationalCommand.ExecuteReader( diff --git a/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryCompilingExpressionVisitor.cs index 91d18ea2c12..7a6162f3e0c 100644 --- a/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -2,20 +2,26 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; +using System.Data.Common; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Pipeline; using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline { public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQueryCompilingExpressionVisitor { private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; private readonly Type _contextType; private readonly IDiagnosticsLogger _logger; private static ParameterExpression _resultCoordinatorParameter @@ -24,6 +30,8 @@ private static ParameterExpression _resultCoordinatorParameter public RelationalShapedQueryCompilingExpressionVisitor( IEntityMaterializerSource entityMaterializerSource, IQuerySqlGeneratorFactory2 querySqlGeneratorFactory, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory, Type contextType, IDiagnosticsLogger logger, bool trackQueryResults, @@ -31,6 +39,8 @@ public RelationalShapedQueryCompilingExpressionVisitor( : base(entityMaterializerSource, trackQueryResults, async) { _querySqlGeneratorFactory = querySqlGeneratorFactory; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; _contextType = contextType; _logger = logger; } @@ -45,6 +55,31 @@ protected override Expression VisitShapedQueryExpression(ShapedQueryExpression s shaperBody = new IncludeCompilingExpressionVisitor(TrackQueryResults).Visit(shaperBody); + if (selectExpression.IsNonComposedFromSql()) + { + var indexMapParameter = Expression.Parameter(typeof(int[]), "indexMap"); + shaperBody = new IndexMapInjectingExpressionVisitor(indexMapParameter).Visit(shaperBody); + var remappedShaperLambda = + Expression.Lambda( + shaperBody, + QueryCompilationContext2.QueryContextParameter, + RelationalProjectionBindingRemovingExpressionVisitor.DataReaderParameter, + indexMapParameter); + + return Expression.New( + Async + ? typeof(FromSqlNonComposedAsyncQueryingEnumerable<>).MakeGenericType(remappedShaperLambda.ReturnType.GetGenericArguments().Single()).GetConstructors()[0] + : typeof(FromSqlNonComposedQueryingEnumerable<>).MakeGenericType(remappedShaperLambda.ReturnType).GetConstructors()[0], + Expression.Convert(QueryCompilationContext2.QueryContextParameter, typeof(RelationalQueryContext)), + Expression.Constant(_querySqlGeneratorFactory), + Expression.Constant(_sqlExpressionFactory), + Expression.Constant(_parameterNameGeneratorFactory), + Expression.Constant(selectExpression), + Expression.Constant(remappedShaperLambda.Compile()), + Expression.Constant(_contextType), + Expression.Constant(_logger)); + } + var shaperLambda = Expression.Lambda( shaperBody, QueryCompilationContext2.QueryContextParameter, @@ -57,6 +92,8 @@ protected override Expression VisitShapedQueryExpression(ShapedQueryExpression s typeof(AsyncQueryingEnumerable<>).MakeGenericType(shaperLambda.ReturnType.GetGenericArguments().Single()).GetConstructors()[0], Expression.Convert(QueryCompilationContext2.QueryContextParameter, typeof(RelationalQueryContext)), Expression.Constant(_querySqlGeneratorFactory), + Expression.Constant(_sqlExpressionFactory), + Expression.Constant(_parameterNameGeneratorFactory), Expression.Constant(selectExpression), Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), @@ -67,12 +104,41 @@ protected override Expression VisitShapedQueryExpression(ShapedQueryExpression s typeof(QueryingEnumerable<>).MakeGenericType(shaperLambda.ReturnType).GetConstructors()[0], Expression.Convert(QueryCompilationContext2.QueryContextParameter, typeof(RelationalQueryContext)), Expression.Constant(_querySqlGeneratorFactory), + Expression.Constant(_sqlExpressionFactory), + Expression.Constant(_parameterNameGeneratorFactory), Expression.Constant(selectExpression), Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), Expression.Constant(_logger)); } + private class IndexMapInjectingExpressionVisitor : ExpressionVisitor + { + private ParameterExpression _indexMapParameter; + + public IndexMapInjectingExpressionVisitor(ParameterExpression indexMapParameter) + { + _indexMapParameter = indexMapParameter; + } + + protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Object != null + && typeof(DbDataReader).IsAssignableFrom(methodCallExpression.Object.Type)) + { + var indexArgument = methodCallExpression.Arguments[0]; + return methodCallExpression.Update( + methodCallExpression.Object, + new[] + { + Expression.ArrayIndex(_indexMapParameter, indexArgument), + }); + } + + return base.VisitMethodCall(methodCallExpression); + } + } + private class ResultCoordinator { public bool? HasNext { get; set; } diff --git a/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryExpressionVisitorFactory.cs b/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryExpressionVisitorFactory.cs index ab0b318d08e..e1d0747879b 100644 --- a/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryExpressionVisitorFactory.cs +++ b/src/EFCore.Relational/Query/Pipeline/RelationalShapedQueryExpressionVisitorFactory.cs @@ -2,8 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Query.Pipeline; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline { @@ -11,12 +11,19 @@ public class RelationalShapedQueryCompilingExpressionVisitorFactory : IShapedQue { private readonly IEntityMaterializerSource _entityMaterializerSource; private readonly IQuerySqlGeneratorFactory2 _querySqlGeneratorFactory; + private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory; - public RelationalShapedQueryCompilingExpressionVisitorFactory(IEntityMaterializerSource entityMaterializerSource, - IQuerySqlGeneratorFactory2 querySqlGeneratorFactory) + public RelationalShapedQueryCompilingExpressionVisitorFactory( + IEntityMaterializerSource entityMaterializerSource, + IQuerySqlGeneratorFactory2 querySqlGeneratorFactory, + ISqlExpressionFactory sqlExpressionFactory, + IParameterNameGeneratorFactory parameterNameGeneratorFactory) { _entityMaterializerSource = entityMaterializerSource; _querySqlGeneratorFactory = querySqlGeneratorFactory; + _sqlExpressionFactory = sqlExpressionFactory; + _parameterNameGeneratorFactory = parameterNameGeneratorFactory; } public ShapedQueryCompilingExpressionVisitor Create(QueryCompilationContext2 queryCompilationContext) @@ -24,6 +31,8 @@ public ShapedQueryCompilingExpressionVisitor Create(QueryCompilationContext2 que return new RelationalShapedQueryCompilingExpressionVisitor( _entityMaterializerSource, _querySqlGeneratorFactory, + _sqlExpressionFactory, + _parameterNameGeneratorFactory, queryCompilationContext.ContextType, queryCompilationContext.Logger, queryCompilationContext.TrackQueryResults, diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs index c31955caf82..38cb91f3e8c 100644 --- a/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs +++ b/src/EFCore.Relational/Query/Pipeline/SqlExpressionFactory.cs @@ -200,6 +200,11 @@ private SqlExpression ApplyTypeMappingOnSqlBinary( resultType, resultTypeMapping); } + + public virtual RelationalTypeMapping GetTypeMappingForValue(object value) + { + return _typeMappingSource.GetMappingForValue(value); + } #endregion #region Binary diff --git a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs index 7bb3fcd24fa..3ea0a1c5a44 100644 --- a/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/Pipeline/SqlExpressions/SelectExpression.cs @@ -84,6 +84,18 @@ public SelectExpression(IEntityType entityType, string sql, Expression arguments _projectionMapping[new ProjectionMember()] = new EntityProjectionExpression(entityType, fromSqlExpression, false); } + public bool IsNonComposedFromSql() + { + return Limit == null + && Offset == null + && !IsDistinct + && Predicate == null + && Orderings.Count == 0 + && Tables.Count == 1 + && Tables[0] is FromSqlExpression fromSql + && Projection.All(pe => pe.Expression is ColumnExpression column ? ReferenceEquals(column.Table, fromSql) : false); + } + public SqlExpression BindProperty(Expression projectionExpression, IProperty property) { var member = (projectionExpression as ProjectionBindingExpression).ProjectionMember; diff --git a/src/EFCore.Relational/Storage/RelationalCommand.cs b/src/EFCore.Relational/Storage/RelationalCommand.cs index 33a541e2fd8..28adda56d14 100644 --- a/src/EFCore.Relational/Storage/RelationalCommand.cs +++ b/src/EFCore.Relational/Storage/RelationalCommand.cs @@ -419,7 +419,7 @@ protected virtual DbCommand CreateCommand( var command = connection.DbConnection.CreateCommand(); command.CommandText = CommandText; - + if (connection.CurrentTransaction != null) { command.Transaction = connection.CurrentTransaction.GetDbTransaction(); diff --git a/src/EFCore.Relational/Storage/RelationalCommandBuilderExtensions.cs b/src/EFCore.Relational/Storage/RelationalCommandBuilderExtensions.cs index 6db14869ea6..cbe1e165636 100644 --- a/src/EFCore.Relational/Storage/RelationalCommandBuilderExtensions.cs +++ b/src/EFCore.Relational/Storage/RelationalCommandBuilderExtensions.cs @@ -280,7 +280,7 @@ public static IRelationalCommandBuilder AddPropertyParameter( property.FindRelationalMapping(), property)); } - + private sealed class Indenter : IDisposable { private readonly IRelationalCommandBuilder _builder; diff --git a/src/EFCore.Relational/Storage/RelationalSqlGenerationHelper.cs b/src/EFCore.Relational/Storage/RelationalSqlGenerationHelper.cs index 02da8f4596f..98af01b0439 100644 --- a/src/EFCore.Relational/Storage/RelationalSqlGenerationHelper.cs +++ b/src/EFCore.Relational/Storage/RelationalSqlGenerationHelper.cs @@ -51,7 +51,9 @@ public RelationalSqlGenerationHelper([NotNull] RelationalSqlGenerationHelperDepe /// A valid name based on the candidate name. /// public virtual string GenerateParameterName(string name) - => "@" + name; + => name.StartsWith("@") + ? name + : "@" + name; /// /// Writes a valid parameter name for the given candidate name. diff --git a/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs index 022eaf160ea..d418f31060b 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs @@ -101,7 +101,7 @@ from o in context.Set().FromSqlRaw(NormalizeDelimetersInRawString("SELECT } } - [Fact(Skip = "#15750")] + [Fact(Skip = "Issue#15763")] public virtual async Task FromSqlRaw_queryable_multiple_composed_with_closure_parameters() { var startDate = new DateTime(1997, 1, 1); @@ -125,7 +125,7 @@ from o in context.Set().FromSqlRaw( } } - [Fact(Skip = "#15750")] + [Fact(Skip = "Issue#15763")] public virtual async Task FromSqlRaw_queryable_multiple_composed_with_parameters_and_closure_parameters() { var city = "London"; @@ -185,7 +185,7 @@ public virtual async Task FromSqlRaw_queryable_composed_multiple_line_query() } } - [Fact(Skip = "#15750")] + [Fact] public virtual async Task FromSqlRaw_queryable_with_parameters() { var city = "London"; @@ -203,7 +203,7 @@ public virtual async Task FromSqlRaw_queryable_with_parameters() } } - [Fact(Skip = "#15750")] + [Fact] public virtual async Task FromSqlRaw_queryable_with_parameters_and_closure() { var city = "London"; @@ -241,7 +241,7 @@ public virtual async Task FromSqlRaw_queryable_simple_cache_key_includes_query_s } } - [Fact(Skip = "#15750")] + [Fact] public virtual async Task FromSqlRaw_queryable_with_parameters_cache_key_includes_parameters() { var city = "London"; diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs index 3bcaf51fe68..a8c78a48e78 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlQueryTestBase.cs @@ -325,7 +325,7 @@ from o in context.Set().FromSqlRaw(NormalizeDelimetersInRawString("SELECT } } - [Fact(Skip = "#15750")] + [Fact(Skip = "Issue#15763")] public virtual void FromSqlRaw_queryable_multiple_composed_with_closure_parameters() { var startDate = new DateTime(1997, 1, 1); @@ -351,7 +351,7 @@ from o in context.Set().FromSqlRaw( } } - [Fact(Skip = "#15750")] + [Fact(Skip = "Issue#15763")] public virtual void FromSqlRaw_queryable_multiple_composed_with_parameters_and_closure_parameters() { var city = "London"; @@ -434,7 +434,7 @@ public virtual void FromSqlRaw_queryable_composed_multiple_line_query() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_queryable_with_parameters() { var city = "London"; @@ -452,7 +452,7 @@ public virtual void FromSqlRaw_queryable_with_parameters() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_queryable_with_parameters_inline() { using (var context = CreateContext()) @@ -468,7 +468,7 @@ public virtual void FromSqlRaw_queryable_with_parameters_inline() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlInterpolated_queryable_with_parameters_interpolated() { var city = "London"; @@ -487,7 +487,7 @@ public virtual void FromSqlInterpolated_queryable_with_parameters_interpolated() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlInterpolated_queryable_with_parameters_inline_interpolated() { using (var context = CreateContext()) @@ -503,7 +503,7 @@ public virtual void FromSqlInterpolated_queryable_with_parameters_inline_interpo } } - [Fact(Skip = "#15750")] + [Fact(Skip = "Issue#15763")] public virtual void FromSqlInterpolated_queryable_multiple_composed_with_parameters_and_closure_parameters_interpolated() { var city = "London"; @@ -546,7 +546,7 @@ from o in context.Set().FromSqlInterpolated( } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_queryable_with_null_parameter() { uint? reportsTo = null; @@ -556,14 +556,14 @@ public virtual void FromSqlRaw_queryable_with_null_parameter() var actual = context.Set().FromSqlRaw( NormalizeDelimetersInRawString( // ReSharper disable once ExpressionIsAlwaysNull - "SELECT * FROM [Employees] WHERE [ReportsTo] = {0} OR (]ReportsTo] IS NULL AND {0} IS NULL)"), reportsTo) + "SELECT * FROM [Employees] WHERE [ReportsTo] = {0} OR ([ReportsTo] IS NULL AND {0} IS NULL)"), reportsTo) .ToArray(); Assert.Equal(1, actual.Length); } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_queryable_with_parameters_and_closure() { var city = "London"; @@ -600,7 +600,7 @@ public virtual void FromSqlRaw_queryable_simple_cache_key_includes_query_string( } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_queryable_with_parameters_cache_key_includes_parameters() { var city = "London"; @@ -718,7 +718,7 @@ public virtual void FromSqlRaw_composed_with_nullable_predicate() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_with_dbParameter() { using (var context = CreateContext()) @@ -733,7 +733,7 @@ public virtual void FromSqlRaw_with_dbParameter() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_with_dbParameter_mixed() { using (var context = CreateContext()) @@ -795,7 +795,7 @@ public virtual void Include_does_not_close_user_opened_connection_for_empty_resu Fixture.TestStore.OpenConnection(); } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_with_db_parameters_called_multiple_times() { using (var context = CreateContext()) @@ -897,7 +897,7 @@ public virtual void Include_closed_connection_opened_by_it_when_buffering() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlInterpolated_with_inlined_db_parameter() { using (var context = CreateContext()) @@ -910,7 +910,7 @@ public virtual void FromSqlInterpolated_with_inlined_db_parameter() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlInterpolated_parameterization_issue_12213() { using (var context = CreateContext()) @@ -940,7 +940,7 @@ public virtual void FromSqlInterpolated_parameterization_issue_12213() } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_does_not_parameterize_interpolated_string() { using (var context = CreateContext()) diff --git a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs index 21388a9e8d9..b853cf9f819 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/FromSqlSprocQueryTestBase.cs @@ -56,7 +56,7 @@ public virtual void From_sql_queryable_stored_procedure_projection() } } - [Fact] + [Fact(Skip = "Issue#15889")] public virtual void From_sql_queryable_stored_procedure_reprojection() { using (var context = CreateContext()) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AsyncFromSqlSprocQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AsyncFromSqlSprocQuerySqlServerTest.cs index ec63bf07091..9cac65a94c2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AsyncFromSqlSprocQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AsyncFromSqlSprocQuerySqlServerTest.cs @@ -5,7 +5,7 @@ namespace Microsoft.EntityFrameworkCore.Query { - internal class AsyncFromSqlSprocQuerySqlServerTest : AsyncFromSqlSprocQueryTestBase> + public class AsyncFromSqlSprocQuerySqlServerTest : AsyncFromSqlSprocQueryTestBase> { public AsyncFromSqlSprocQuerySqlServerTest(NorthwindQuerySqlServerFixture fixture) : base(fixture) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs index 49449549a3b..2278cf3abcc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlQuerySqlServerTest.cs @@ -525,7 +525,7 @@ public override void FromSqlInterpolated_with_inlined_db_parameter() SELECT * FROM ""Customers"" WHERE ""CustomerID"" = @somename"); } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_in_subquery_with_dbParameter() { using (var context = CreateContext()) @@ -557,7 +557,7 @@ SELECT [c].[CustomerID] } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_in_subquery_with_positional_dbParameter_without_name() { using (var context = CreateContext()) @@ -592,7 +592,7 @@ SELECT [c].[CustomerID] } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_in_subquery_with_positional_dbParameter_with_name() { using (var context = CreateContext()) @@ -624,7 +624,7 @@ SELECT [c].[CustomerID] } } - [Fact(Skip = "#15750")] + [Fact] public virtual void FromSqlRaw_with_dbParameter_mixed_in_subquery() { using (var context = CreateContext()) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs index e7c095cf747..1f4aaf89fb9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/FromSqlSprocQuerySqlServerTest.cs @@ -8,7 +8,7 @@ namespace Microsoft.EntityFrameworkCore.Query { - internal class FromSqlSprocQuerySqlServerTest : FromSqlSprocQueryTestBase> + public class FromSqlSprocQuerySqlServerTest : FromSqlSprocQueryTestBase> { public FromSqlSprocQuerySqlServerTest( NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) @@ -40,7 +40,7 @@ public override void From_sql_queryable_stored_procedure_with_parameter() base.From_sql_queryable_stored_procedure_with_parameter(); Assert.Equal( - @"@p0='ALFKI' (Size = 4000) + @"p0='ALFKI' (Size = 4000) [dbo].[CustOrderHist] @CustomerID = @p0", Sql, diff --git a/test/EFCore.SqlServer.FunctionalTests/SqlServerComplianceTest.cs b/test/EFCore.SqlServer.FunctionalTests/SqlServerComplianceTest.cs index b89e8fcd37c..fdae32b8bd2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/SqlServerComplianceTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/SqlServerComplianceTest.cs @@ -31,8 +31,6 @@ public class SqlServerComplianceTest : RelationalComplianceTestBase typeof(GearsOfWarFromSqlQueryTestBase<>), typeof(QueryNoClientEvalTestBase<>), typeof(WarningsTestBase<>), - typeof(AsyncFromSqlSprocQueryTestBase<>), - typeof(FromSqlSprocQueryTestBase<>), }; protected override Assembly TargetAssembly { get; } = typeof(SqlServerComplianceTest).Assembly;