Skip to content

Commit

Permalink
Implement SqlQuery<T>
Browse files Browse the repository at this point in the history
Resolves #11624
  • Loading branch information
smitpatel committed Aug 13, 2022
1 parent 7430b39 commit 0bd8b43
Show file tree
Hide file tree
Showing 20 changed files with 519 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -292,6 +293,48 @@ public static int ExecuteSqlRaw(
}
}

/// <summary>
/// .
/// </summary>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="sql">The raw SQL query.</param>
/// <param name="parameters">The values to be assigned to parameters.</param>
/// <returns>An <see cref="IQueryable{T}" /> representing the raw SQL query.</returns>
public static IQueryable<TResult> SqlQueryRaw<TResult>(
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<TResult>(new SqlQueryRootExpression(
facadeDependencies.QueryProvider, typeof(TResult), sql, Expression.Constant(parameters)));
}

/// <summary>
/// .
/// </summary>
/// <param name="databaseFacade">The <see cref="DatabaseFacade" /> for the context.</param>
/// <param name="sql">The interpolated string representing a SQL query with parameters.</param>
/// <returns>An <see cref="IQueryable{T}" /> representing the interpolated string SQL query.</returns>
public static IQueryable<TResult> SqlQuery<TResult>(
this DatabaseFacade databaseFacade,
[NotParameterized] FormattableString sql)
{
Check.NotNull(sql, nameof(sql));
Check.NotNull(sql.Format, nameof(sql.Format));

var facadeDependencies = GetFacadeDependencies(databaseFacade);

return facadeDependencies.QueryProvider
.CreateQuery<TResult>(new SqlQueryRootExpression(
facadeDependencies.QueryProvider, typeof(TResult), sql.Format, Expression.Constant(sql.GetArguments())));
}

/// <summary>
/// Executes the given SQL against the database and returns the number of rows affected.
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@
<value>The required column '{column}' was not present in the results of a 'FromSql' operation.</value>
</data>
<data name="FromSqlNonComposable" xml:space="preserve">
<value>'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.</value>
<value>'FromSql' 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.</value>
</data>
<data name="FunctionOverrideMismatch" xml:space="preserve">
<value>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}'.</value>
Expand Down Expand Up @@ -951,6 +951,9 @@
<data name="SqlQueryOverrideMismatch" xml:space="preserve">
<value>The property '{propertySpecification}' has specific configuration for the SQL query '{query}', but isn't mapped to a column on that query. Remove the specific configuration, or map an entity type that contains this property to '{query}'.</value>
</data>
<data name="SqlQueryUnmappedType" xml:space="preserve">
<value>The element type '{elementType}' used in 'SqlQuery' method is not mapped in current type provider.</value>
</data>
<data name="StoredProcedureConcurrencyTokenNotMapped" xml:space="preserve">
<value>The entity type '{entityType}' is mapped to the stored procedure '{sproc}', but the concurrency token '{token}' is not mapped to any original value parameter.</value>
</data>
Expand Down
7 changes: 6 additions & 1 deletion src/EFCore.Relational/Query/Internal/BufferedDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ public static int[] BuildIndexMap(IReadOnlyList<string> 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;
Expand Down
125 changes: 125 additions & 0 deletions src/EFCore.Relational/Query/Internal/SqlQueryRootExpression.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public sealed class SqlQueryRootExpression : QueryRootExpression
{
/// <summary>
/// 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.
/// </summary>
public SqlQueryRootExpression(
IAsyncQueryProvider queryProvider,
Type elementType,
string sql,
Expression argument)
: base(queryProvider, elementType)
{
Sql = sql;
Argument = argument;
}

/// <summary>
/// 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.
/// </summary>
public SqlQueryRootExpression(
Type elementType,
string sql,
Expression argument)
: base(elementType)
{
Sql = sql;
Argument = argument;
}

/// <summary>
/// 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.
/// </summary>
public string Sql { get; }

/// <summary>
/// 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.
/// </summary>
public Expression Argument { get; }

/// <summary>
/// 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.
/// </summary>
public override Expression DetachQueryProvider()
=> new SqlQueryRootExpression(ElementType, Sql, Argument);

/// <summary>
/// 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.
/// </summary>
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var argument = visitor.Visit(Argument);

return argument != Argument
? new SqlQueryRootExpression(ElementType, Sql, argument)
: this;
}

/// <summary>
/// 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.
/// </summary>
protected override void Print(ExpressionPrinter expressionPrinter)
{
expressionPrinter.Append($"SqlQuery<{ElementType.ShortDisplayName()}>({Sql}, ");
expressionPrinter.Visit(Argument);
expressionPrinter.AppendLine(")");
}

/// <summary>
/// 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.
/// </summary>
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);

/// <summary>
/// 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.
/// </summary>
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), Sql, ExpressionEqualityComparer.Instance.GetHashCode(Argument));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 FromSqlExpression("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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
Expand All @@ -62,4 +64,9 @@ public RelationalQueryableMethodTranslatingExpressionVisitorDependencies(
/// The SQL expression factory.
/// </summary>
public ISqlExpressionFactory SqlExpressionFactory { get; init; }

/// <summary>
/// The relational type mapping souce.
/// </summary>
public IRelationalTypeMappingSource TypeMappingSource { get; init; }
}
Loading

0 comments on commit 0bd8b43

Please sign in to comment.