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

Tables: Query support for dictionary entities and CreateFilter #12366

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ protected TableClient() { }
public virtual Azure.Response<T> Insert<T>(T entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.Response Merge(System.Collections.Generic.IDictionary<string, object> entity, string eTag = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response> MergeAsync(System.Collections.Generic.IDictionary<string, object> entity, string eTag = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Pageable<System.Collections.Generic.IDictionary<string, object>> Query(string select = null, string filter = null, int? top = default(int?), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.AsyncPageable<System.Collections.Generic.IDictionary<string, object>> QueryAsync(string select = null, string filter = null, int? top = default(int?), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.AsyncPageable<T> QueryAsync<T>(System.Linq.Expressions.Expression<System.Func<T, bool>> filter, string select = null, int? top = default(int?), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.Pageable<System.Collections.Generic.IDictionary<string, object>> Query(System.Linq.Expressions.Expression<System.Func<System.Collections.Generic.IDictionary<string, object>, bool>> filter, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Pageable<System.Collections.Generic.IDictionary<string, object>> Query(string filter = null, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.AsyncPageable<System.Collections.Generic.IDictionary<string, object>> QueryAsync(System.Linq.Expressions.Expression<System.Func<System.Collections.Generic.IDictionary<string, object>, bool>> filter, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.AsyncPageable<System.Collections.Generic.IDictionary<string, object>> QueryAsync(string filter = null, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.AsyncPageable<T> QueryAsync<T>(System.Linq.Expressions.Expression<System.Func<T, bool>> filter, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.AsyncPageable<T> QueryAsync<T>(string filter = null, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.Pageable<T> Query<T>(System.Linq.Expressions.Expression<System.Func<T, bool>> filter, string select = null, int? top = default(int?), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.Pageable<T> Query<T>(System.Linq.Expressions.Expression<System.Func<T, bool>> filter, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.Pageable<T> Query<T>(string filter = null, int? top = default(int?), string select = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) where T : Azure.Data.Tables.TableEntity, new() { throw null; }
public virtual Azure.Response SetAccessPolicy(System.Collections.Generic.IEnumerable<Azure.Data.Tables.Models.SignedIdentifier> tableAcl, int? timeout = default(int?), string requestId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response> SetAccessPolicyAsync(System.Collections.Generic.IEnumerable<Azure.Data.Tables.Models.SignedIdentifier> tableAcl = null, int? timeout = default(int?), string requestId = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Expand Down Expand Up @@ -219,6 +221,13 @@ internal TableServiceStats() { }
public Azure.Data.Tables.Models.GeoReplication GeoReplication { get { throw null; } }
}
}
namespace Azure.Data.Tables.Queryable
{
public static partial class TableClientExtensions
{
public static string CreateFilter<T>(this Azure.Data.Tables.TableClient client, System.Linq.Expressions.Expression<System.Func<T, bool>> filter) { throw null; }
}
}
namespace Azure.Data.Tables.Sas
{
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Azure.Data.Tables.Queryable
{
public static class TableClientExtensions
{
/// <summary>
/// Creates an Odata filter query string from the provided expression.
/// </summary>
/// <typeparam name="T">The type of the entity being queried. Typically this will be derrived from <see cref="TableEntity"/> or <see cref="Dictionary{String, Object}"/>.</typeparam>
/// <param name="client">The <see cref="TableClient"/>.</param>
/// <param name="filter">A filter expresssion.</param>
/// <returns>The string representation of the filter expression.</returns>
public static string CreateFilter<T>(this TableClient client, Expression<Func<T, bool>> filter) => client.Bind(filter);
}
}
29 changes: 21 additions & 8 deletions sdk/tables/Azure.Data.Tables/src/Queryable/ExpressionNormalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,22 @@ internal override Expression VisitBinary(BinaryExpression b)
{
BinaryExpression visited = (BinaryExpression)base.VisitBinary(b);

if (visited.NodeType == ExpressionType.Equal)
switch (visited.NodeType)
{
Expression normalizedLeft = UnwrapObjectConvert(visited.Left);
Expression normalizedRight = UnwrapObjectConvert(visited.Right);
if (normalizedLeft != visited.Left || normalizedRight != visited.Right)
{
visited = CreateRelationalOperator(ExpressionType.Equal, normalizedLeft, normalizedRight);
}
case ExpressionType.Equal:
case ExpressionType.NotEqual:
case ExpressionType.LessThan:
case ExpressionType.LessThanOrEqual:
case ExpressionType.GreaterThan:
case ExpressionType.GreaterThanOrEqual:

Expression normalizedLeft = UnwrapObjectConvert(visited.Left);
Expression normalizedRight = UnwrapObjectConvert(visited.Right);
if (normalizedLeft != visited.Left || normalizedRight != visited.Right)
{
visited = CreateRelationalOperator(visited.NodeType, normalizedLeft, normalizedRight);
}
break;
}

if (_patterns.TryGetValue(visited.Left, out Pattern pattern) && pattern.Kind == PatternKind.Compare && IsConstantZero(visited.Right))
Expand Down Expand Up @@ -84,7 +92,7 @@ private static Expression UnwrapObjectConvert(Expression input)
}
}

while (ExpressionType.Convert == input.NodeType && typeof(object) == input.Type)
while (ExpressionType.Convert == input.NodeType)
{
input = ((UnaryExpression)input).Operand;
}
Expand Down Expand Up @@ -134,6 +142,11 @@ internal Expression VisitMethodCallNoRewrite(MethodCallExpression call)
return CreateCompareExpression(visited.Arguments[0], visited.Arguments[1]);
}

if (visited.Method == ReflectionUtil.DictionaryGetItemMethodInfo && visited.Arguments.Count == 1 && visited.Arguments[0] is ConstantExpression ce)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at ExpressionNormalizer and ExpressionWriter I wonder if we really need both or if they can be merged into one with additional benefit of avoiding creating of more Expression nodes just to throw them away.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some cases the normalizer is simplifying the expression to be written by the writer. I'd like to do an optimization pass at some point to address issues like this and others.

{
return visited;
}

throw new NotSupportedException($"Method {visited.Method.Name} not supported.");
}

Expand Down
27 changes: 24 additions & 3 deletions sdk/tables/Azure.Data.Tables/src/Queryable/ExpressionWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ internal override Expression Visit(Expression exp)
return result;
}

internal override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method == ReflectionUtil.DictionaryGetItemMethodInfo && m.Arguments.Count == 1 && m.Arguments[0] is ConstantExpression ce)
{
_builder.Append(ce.Value as string);
}
else
{
return base.VisitMethodCall(m);
}

return m;
}

internal override Expression VisitMemberAccess(MemberExpression m)
{
Expand Down Expand Up @@ -155,9 +168,17 @@ private void VisitOperand(Expression e)
{
if (e is BinaryExpression || e is UnaryExpression)
{
_builder.Append(UriHelper.LEFTPAREN);
Visit(e);
_builder.Append(UriHelper.RIGHTPAREN);
if (e is UnaryExpression unary && unary.NodeType == ExpressionType.TypeAs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a functional benefit in dropping the parenthesis for casts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the parens are not useful. The cast is only needed for the comparison to satisfy the compiler in that all dictionary entries are object so the writer is basically ignoring it. Leaving the parens here results in something like:
(PartitionKey) eq "foo"

{
Visit(unary.Operand);
}
else
{
_builder.Append(UriHelper.LEFTPAREN);
Visit(e);
_builder.Append(UriHelper.RIGHTPAREN);
}

}
else
{
Expand Down
18 changes: 18 additions & 0 deletions sdk/tables/Azure.Data.Tables/src/Queryable/ReflectionUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Generic;
using System.Reflection;

namespace Azure.Data.Tables.Queryable
{
internal static class ReflectionUtil
{
internal static MethodInfo DictionaryGetItemMethodInfo { get; }

static ReflectionUtil()
{
DictionaryGetItemMethodInfo = typeof(IDictionary<string, object>).GetMethod("get_Item");
}
}
}
31 changes: 22 additions & 9 deletions sdk/tables/Azure.Data.Tables/src/TableClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,13 +408,13 @@ public virtual Response Merge(IDictionary<string, object> entity, string eTag =
/// <summary>
/// Queries entities in the table.
/// </summary>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="filter">Returns only tables or entities that satisfy the specified filter.</param>
/// <param name="top">Returns only the top n tables or entities from the set.</param>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
/// <returns></returns>
[ForwardsClientCalls]
public virtual AsyncPageable<IDictionary<string, object>> QueryAsync(string select = null, string filter = null, int? top = null, CancellationToken cancellationToken = default)
public virtual AsyncPageable<IDictionary<string, object>> QueryAsync(string filter = null, int? top = null, string select = null, CancellationToken cancellationToken = default)
{
return PageableHelpers.CreateAsyncEnumerable(async _ =>
{
Expand Down Expand Up @@ -450,13 +450,13 @@ public virtual AsyncPageable<IDictionary<string, object>> QueryAsync(string sele
/// <summary>
/// Queries entities in the table.
/// </summary>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="filter">Returns only tables or entities that satisfy the specified filter.</param>
/// <param name="top">Returns only the top n tables or entities from the set.</param>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>

[ForwardsClientCalls]
public virtual Pageable<IDictionary<string, object>> Query(string select = null, string filter = null, int? top = null, CancellationToken cancellationToken = default)
public virtual Pageable<IDictionary<string, object>> Query(string filter = null, int? top = null, string select = null, CancellationToken cancellationToken = default)
{
return PageableHelpers.CreateEnumerable(_ =>
{
Expand Down Expand Up @@ -493,14 +493,27 @@ public virtual Pageable<IDictionary<string, object>> Query(string select = null,
/// Queries entities in the table.
/// </summary>
/// <param name="filter">Returns only tables or entities that satisfy the specified filter.</param>
/// <param name="top">Returns only the top n tables or entities from the set.</param>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>

[ForwardsClientCalls]
public virtual AsyncPageable<IDictionary<string, object>> QueryAsync(Expression<Func<IDictionary<string, object>, bool>> filter, int? top = null, string select = null, CancellationToken cancellationToken = default) =>
QueryAsync(Bind(filter), top, select, cancellationToken);

public virtual Pageable<IDictionary<string, object>> Query(Expression<Func<IDictionary<string, object>, bool>> filter, int? top = null, string select = null, CancellationToken cancellationToken = default) =>
Query(Bind(filter), top, select, cancellationToken);

/// <summary>
/// Queries entities in the table.
/// </summary>
/// <param name="filter">Returns only tables or entities that satisfy the specified filter.</param>
/// <param name="top">Returns only the top n tables or entities from the set.</param>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>

[ForwardsClientCalls]
#pragma warning disable AZC0004 // DO provide both asynchronous and synchronous variants for all service methods.
public virtual AsyncPageable<T> QueryAsync<T>(Expression<Func<T, bool>> filter, string select = null, int? top = null, CancellationToken cancellationToken = default) where T : TableEntity, new() =>
#pragma warning restore AZC0004 // DO provide both asynchronous and synchronous variants for all service methods.
public virtual AsyncPageable<T> QueryAsync<T>(Expression<Func<T, bool>> filter, int? top = null, string select = null, CancellationToken cancellationToken = default) where T : TableEntity, new() =>
QueryAsync<T>(Bind(filter), top, select, cancellationToken);

/// <summary>
Expand Down Expand Up @@ -545,12 +558,12 @@ public virtual Pageable<IDictionary<string, object>> Query(string select = null,
/// Queries entities in the table.
/// </summary>
/// <param name="filter">Returns only tables or entities that satisfy the specified filter.</param>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="top">Returns only the top n tables or entities from the set.</param>
/// <param name="select">Returns the desired properties of an entity from the set. </param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>

[ForwardsClientCalls]
public virtual Pageable<T> Query<T>(Expression<Func<T, bool>> filter, string select = null, int? top = null, CancellationToken cancellationToken = default) where T : TableEntity, new() =>
public virtual Pageable<T> Query<T>(Expression<Func<T, bool>> filter, int? top = null, string select = null, CancellationToken cancellationToken = default) where T : TableEntity, new() =>
Query<T>(Bind(filter), top, select, cancellationToken);

/// <summary>
Expand Down
Loading