Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmos FromSql #25525

Merged
1 commit merged into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
roji marked this conversation as resolved.
Show resolved Hide resolved
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