Skip to content

Commit

Permalink
Cosmos FromSql (#25525)
Browse files Browse the repository at this point in the history
Closes #17311
  • Loading branch information
roji authored Aug 17, 2021
1 parent f4f35c2 commit c3e2340
Show file tree
Hide file tree
Showing 23 changed files with 1,238 additions and 122 deletions.
55 changes: 54 additions & 1 deletion src/EFCore.Cosmos/Extensions/CosmosQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Utilities;
Expand All @@ -13,7 +15,7 @@
namespace Microsoft.EntityFrameworkCore
{
/// <summary>
/// Cosmos DB specific extension methods for LINQ queries.
/// Cosmos-specific extension methods for LINQ queries.
/// </summary>
public static class CosmosQueryableExtensions
{
Expand Down Expand Up @@ -46,5 +48,56 @@ source.Provider is EntityQueryProvider
Expression.Constant(partitionKey)))
: source;
}

/// <summary>
/// <para>
/// Creates a LINQ query based on a raw SQL query.
/// </para>
/// <para>
/// You can compose on top of the raw SQL query using LINQ operators:
/// </para>
/// <code>context.Blogs.FromSqlRaw("SELECT * FROM root c).OrderBy(b => b.Name)</code>
/// <para>
/// As with any API that accepts SQL it is important to parameterize any user input to protect against a SQL injection
/// attack. You can include parameter place holders in the SQL query string and then supply parameter values as additional
/// arguments. Any parameter values you supply will automatically be converted to a Cosmos parameter:
/// </para>
/// <code>context.Blogs.FromSqlRaw(""SELECT * FROM root c WHERE c["Name"] = {0})", userSuppliedSearchTerm)</code>
/// </summary>
/// <typeparam name="TEntity"> The type of the elements of <paramref name="source" />. </typeparam>
/// <param name="source">
/// An <see cref="IQueryable{T}" /> to use as the base of the raw SQL query (typically a <see cref="DbSet{TEntity}" />).
/// </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>
[StringFormatMethod("sql")]
public static IQueryable<TEntity> FromSqlRaw<TEntity>(
this IQueryable<TEntity> source,
[NotParameterized] string sql,
params object[] parameters)
where TEntity : class
{
Check.NotNull(source, nameof(source));
Check.NotEmpty(sql, nameof(sql));
Check.NotNull(parameters, nameof(parameters));

var queryRootExpression = (QueryRootExpression)source.Expression;

var entityType = queryRootExpression.EntityType;

Check.DebugAssert(
(entityType.BaseType is null && !entityType.GetDirectlyDerivedTypes().Any())
|| entityType.FindDiscriminatorProperty() is not null,
"Found FromSql on a TPT entity type, but TPT isn't supported on Cosmos");

var fromSqlQueryRootExpression = new FromSqlQueryRootExpression(
queryRootExpression.QueryProvider!,
entityType,
sql,
Expression.Constant(parameters));

return source.Provider.CreateQuery<TEntity>(fromSqlQueryRootExpression);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,14 @@ private void ProcessEntityType(IConventionEntityTypeBuilder entityTypeBuilder)
return;
}

if (!entityType.IsDocumentRoot())
if (entityType.IsDocumentRoot())
{
entityTypeBuilder.HasNoDiscriminator();
entityTypeBuilder.HasDiscriminator(typeof(string))
?.HasValue(entityType, entityType.ShortName());
}
else
{
entityTypeBuilder.HasDiscriminator(typeof(string))
?.HasValue(entityType, entityType.ShortName());
entityTypeBuilder.HasNoDiscriminator();
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

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

3 changes: 3 additions & 0 deletions src/EFCore.Cosmos/Properties/CosmosStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
<data name="InvalidDerivedTypeInEntityProjection" xml:space="preserve">
<value>The specified entity type '{derivedType}' is not derived from '{entityType}'.</value>
</data>
<data name="InvalidFromSqlArguments" xml:space="preserve">
<value>A FromSqlExpression has an invalid arguments expression type '{expressionType}' or value type '{valueType}'.</value>
</data>
<data name="InvalidResourceId" xml:space="preserve">
<value>Unable to generate a valid 'id' value to execute a 'ReadItem' query. This usually happens when the value provided for one of the properties is 'null' or an empty string. Please supply a value that's not 'null' or an empty string.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ public override Expression Process(Expression query)
{
query = base.Process(query);

if (query is ShapedQueryExpression shapedQueryExpression
&& shapedQueryExpression.QueryExpression is SelectExpression selectExpression)
if (query is ShapedQueryExpression { QueryExpression: SelectExpression selectExpression })
{
// Cosmos does not have nested select expression so this should be safe.
selectExpression.ApplyProjection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public override Expression Visit(Expression expression)

var readItemExpression = new ReadItemExpression(entityType, propertyParameterList);

return CreateShapedQueryExpression(readItemExpression, entityType)
return CreateShapedQueryExpression(entityType, readItemExpression)
.UpdateResultCardinality(ResultCardinality.Single);
}
}
Expand Down Expand Up @@ -187,6 +187,24 @@ static bool TryGetPartitionKeyProperty(IEntityType entityType, out IProperty par
}
}

/// <inheritdoc />
protected override Expression VisitExtension(Expression extensionExpression)
{
switch (extensionExpression)
{
case FromSqlQueryRootExpression fromSqlQueryRootExpression:
return CreateShapedQueryExpression(
fromSqlQueryRootExpression.EntityType,
_sqlExpressionFactory.Select(
fromSqlQueryRootExpression.EntityType,
fromSqlQueryRootExpression.Sql,
fromSqlQueryRootExpression.Argument));

default:
return base.VisitExtension(extensionExpression);
}
}

/// <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
Expand Down Expand Up @@ -246,10 +264,10 @@ protected override ShapedQueryExpression CreateShapedQueryExpression(IEntityType

var selectExpression = _sqlExpressionFactory.Select(entityType);

return CreateShapedQueryExpression(selectExpression, entityType);
return CreateShapedQueryExpression(entityType, selectExpression);
}

private ShapedQueryExpression CreateShapedQueryExpression(Expression queryExpression, IEntityType entityType)
private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType, Expression queryExpression)
=> new(
queryExpression,
new EntityShaperExpression(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery
switch (shapedQueryExpression.QueryExpression)
{
case SelectExpression selectExpression:

shaperBody = new CosmosProjectionBindingRemovingExpressionVisitor(
selectExpression, jObjectParameter,
QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll)
Expand All @@ -92,7 +91,6 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery
Expression.Constant(_threadSafetyChecksEnabled));

case ReadItemExpression readItemExpression:

shaperBody = new CosmosProjectionBindingRemovingReadItemExpressionVisitor(
readItemExpression, jObjectParameter,
QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll)
Expand Down
108 changes: 108 additions & 0 deletions src/EFCore.Cosmos/Query/Internal/FromSqlExpression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Utilities;

#nullable disable

namespace Microsoft.EntityFrameworkCore.Cosmos.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 class FromSqlExpression : RootReferenceExpression, IPrintableExpression
{
/// <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 FromSqlExpression(IEntityType entityType, string alias, string sql, Expression arguments) : base(entityType, alias)
{
Check.NotEmpty(sql, nameof(sql));
Check.NotNull(arguments, nameof(arguments));

Sql = sql;
Arguments = arguments;
}

/// <inheritdoc />
public override string Alias => base.Alias!;

/// <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 virtual 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 virtual Expression Arguments { 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 virtual FromSqlExpression Update(Expression arguments)
{
Check.NotNull(arguments, nameof(arguments));

return arguments != Arguments
? new FromSqlExpression(EntityType, Alias, Sql, arguments)
: this;
}

/// <inheritdoc />
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));

return this;
}

/// <inheritdoc />
public override Type Type
=> typeof(object);

/// <inheritdoc />
void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
{
Check.NotNull(expressionPrinter, nameof(expressionPrinter));

expressionPrinter.Append(Sql);
}

/// <inheritdoc />
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)
&& Sql == fromSqlExpression.Sql
&& ExpressionEqualityComparer.Instance.Equals(Arguments, fromSqlExpression.Arguments);

/// <inheritdoc />
public override int GetHashCode()
=> HashCode.Combine(base.GetHashCode(), Sql);
}
}
Loading

0 comments on commit c3e2340

Please sign in to comment.