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

Fix to #6647 - Left Join (GroupJoin) always materializes elements resulting in unnecessary data pulling #7772

Merged
merged 1 commit into from
Mar 15, 2017

Conversation

maumar
Copy link
Contributor

@maumar maumar commented Mar 3, 2017

Problem was that for GroupJoin we would always force materialization on participating query sources.
We were doing this because we are not always able to correctly divide outer elements of the GroupJoin into correct groups.
However, if the GroupJoin clause is wrapped around SelectMany clause then groups don't matter because they are getting flattened by SelectMany anyway.

Fix is to recognize those scenarios and only force materialization when the correct grouping actually matters.
We can avoid materialization if the GroupJoin is followed by SelectMany clause (that references the grouping) and that the grouping itself is not present anywhere else in the query.
This addresses optional navigations, which is the 80% case. Manually created GroupJoins that are not modeling LeftOuterJoins still require additional materialization, but this can be addressed later as the priority is not nearly as high.

This change also addresses #7722 - Query : error during compilation for queries with navigation properties and First/Single/client method operators inside a subquery.

Problem here was that for some queries we don't know how to properly bind to a value buffer (when the result of binding is subquery, and not qsre).
Fix/mitigation is to recognize those scenarios and force materialization on the subqueries. This can be properly addressed in later commit (i.e. by improving the binding logic)

Also fixed several minor issues that were uncovered since we no longer do extensive materialization.

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

@anpete @smitpatel

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

@tuespetre fyi, feel free to chime in as you have significant knowledge in this area

}
}

private Expression ExtractSourceExpression(Expression expression)
Copy link
Contributor Author

@maumar maumar Mar 3, 2017

Choose a reason for hiding this comment

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

This code has been copied and modified from the EntityQueryModelVisitor.IterateCompositePropertyExpression Might be worthwhile to add a "debug"/"mock" binding to EQMV. Wasn't sure if its worth to add public surface to that class - this approach is sort of a hack, ideally we would fix the binding itself to work for those scenarios. That's why I opted for copying the code into a private method that can be removed without issue once a proper fix is implemented. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Not much need for this, once inside the BindMemberExpression/BindMethodCallExpression lambda, there can only have been a single property, so RemoveConvert() on node.Expression or node.Arguments[0] suffices

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call!

@@ -318,6 +318,8 @@ var entityEqualityRewritingExpressionVisitor
_navigationRewritingExpressionVisitorFactory
.Create(this).Rewrite(queryModel, parentQueryModel: null);

entityEqualityRewritingExpressionVisitor.Rewrite(queryModel);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we avoid rewrite twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can try, but IIRC we were missing some cases if the equality rewrite happens only after nav rewrite

Copy link
Contributor

Choose a reason for hiding this comment

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

why does nav rewrite depends on equality rewrite?

Copy link
Contributor Author

@maumar maumar Mar 3, 2017

Choose a reason for hiding this comment

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

Currently some scenarios involving navigation entity comparison is broken if entity equality doesn't run before nav rewrite. We could probably refactor that code by adding some intermediate step between nav rewrite and entity comparison, filed #7773 to track this

}
}

private class RequiresMaterializationCompensatingVisitorState
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can split this into individual properties and pass them directly. In earlier iterations there was a boolean state that was shared between query model visitor and expression visitor so passing a reference was needed. Now it can be rewritten, but not sure if it's worth - this "nicely" encapsulates all the data/helpers that those visitors need. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

No need to pass the state in -- the QueryModelVisitor and the ExpressionVisitor each can have a public HashSet<IQuerySource> QuerySources { get; } that they add to. In FindQuerySourcesRequiringMaterialization, the resulting sets from all three visitors (the original + two compensating) can be combined

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, simplifies this code a lot, now that both visitors don't share state


protected virtual IQuerySource PreProcessQuerySource(IQuerySource querySource)
{
var newQuerySource = ExtractGroupJoinFromSelectManySubquery(querySource);
Copy link
Contributor

@tuespetre tuespetre Mar 3, 2017

Choose a reason for hiding this comment

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

Could the same result be achieved by assigning the appropriate QuerySource to joinExpression in OptimizeJoinClause (assign it the joinClause, and then if flattening a DefaultIfEmpty, assign it the additionalFromClause instead)?

Copy link
Contributor

@tuespetre tuespetre Mar 3, 2017

Choose a reason for hiding this comment

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

@maumar these changes work but keep the 'hairiness' out of TableExpressionBase:

6a20802

It should be noted that the extra-hair-tastic bit (the else if block) is only required because we don't lift all the subqueries we could yet (i.e. lifting _Select using a ProjectionShaper) and thus the group joins aren't flattened as they could be.

Copy link
Contributor Author

@maumar maumar Mar 3, 2017

Choose a reason for hiding this comment

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

@smitpatel @anpete any thoughts on this? - you guys know this area better than me ;) One thing worth pointing out is that we can't rely on ProjectionShaper optimization to simplify the code, since there can always be client method involved

Copy link
Contributor

Choose a reason for hiding this comment

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

Noting before I forget: The block itself was only needed to make two tests pass: GroupJoin_on_left_side_being_a_subquery and its twin.

if (sourceExpression is SubQueryExpression subQueryExpression)
{
var queryModel = subQueryExpression.QueryModel;
if (queryModel.ResultOperators.ToList().OfType<ChoiceResultOperatorBase>().Any())
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't need to check the result operators -- if it's a subquery bound to a property, you can just promote the selector as a QSRE, using HandleUnderlyingQuerySources pointing at a MarkForMaterialization method that adds them to the collection

Copy link
Contributor Author

Choose a reason for hiding this comment

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

is selector guaranteed to be QSRE? Might be safer to just delegate to the requiresmaterialization visitor, but I agree that there is no need to check for result operators - (I think) there should always be one if we end up in this situation

Copy link
Contributor

@tuespetre tuespetre Mar 3, 2017

Choose a reason for hiding this comment

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

If it's an entity property access, I would think so (or it's another subquery that returns an entity, which HandleUnderlyingQuerySources should handle) BUT now that you bring it up, what about a subquery that selects an entity via a constructor, like (from c in customers select new Customer() { SingleProp = c.SingleProp }).FirstOrDefault(),SingleProp? I'm not sure I've seen a test that covers that already.

EDIT: Thought occurred, and that case wouldn't matter -- because it would return a Customer object, not a value buffer.

@tuespetre
Copy link
Contributor

@maumar I had to confirm all of my suspicions before making comments, here are the results in case it's of any help to you: fix7290...tuespetre:fix7290

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

new version up

private void CompensateForUnboundPropertyAccess(Expression expression)
{
var sourceExpression = expression.RemoveConvert();
if (sourceExpression is SubQueryExpression subQueryExpression)
Copy link
Contributor

Choose a reason for hiding this comment

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

⛏ : if (expression.RemoveConvert() is SubQueryExpression subQueryExpression)

Copy link
Contributor

Choose a reason for hiding this comment

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

Hold up, new developments coming in

@tuespetre
Copy link
Contributor

@maumar, with a couple small changes, RequiresClientSingleColumnResultOperator can be removed. I also tried to see if the UnboundPropertyAccessCompensatingVisitor could be removed entirely. Here is the result of both: 8416464

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

@tuespetre the change is dangerously growing in complexity ;) I thinking to keep the hack for now as its very simple and localized and will leave #7722 open. Once this change is in, you could submit a new PR with the proper fix and remove the hack.

@tuespetre
Copy link
Contributor

I thought so too, especially the UnboundPropertyAccessCompensatingVisitor. Good call to leave that for afterwards tied to #7722.

The RequiresClientSingleColumnResultOperator change is not complex in itself, though. (2 changed files, other than test SQL output)

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

@tuespetre agreed, especially that it's related to this change - we introduced this flag because it was messing with out groupjoins. Will incorporate this into the PR

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

@tuespetre after doing some more digging, simply removing RequiresClientSingleColumnResultOperator regresses queries like this:

                var query = context.Gears.GroupJoin(
                    context.Tags,
                    g => new { k1 = g.Nickname, k2 = (int?)g.SquadId },
                    t => new { k1 = t.GearNickName, k2 = t.GearSquadId },
                    (g, t) => g.HasSoulPatch).Distinct();

For those we still materialize extra stuff, so distinct needs to happen on the client still.

RequiresClientSingleColumnResultOperator can be dealt with when fixing #7497. I would imagine we will do something similar to the groupjoin compensating visitor - if the groupjoin is followed by appropriate SelectMany and is only present in the query model once, we allow result operators to be translated, otherwise force client eval.

@tuespetre
Copy link
Contributor

Oh, ok. I suppose Distinct on a bool would mask some errors like that!

@maumar
Copy link
Contributor Author

maumar commented Mar 3, 2017

new version up (adding more tests)

@maumar
Copy link
Contributor Author

maumar commented Mar 10, 2017

new version up, rebased on the latest changes and fixed some more bugs found along the way

/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class RecursiveQueryModelVisitor : ExpressionTransformingQueryModelVisitor<RecursiveQueryModelExpressionVisitor>
Copy link
Contributor

Choose a reason for hiding this comment

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

why hello there beautiful 💖

Copy link
Contributor

Choose a reason for hiding this comment

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

@maumar - Is it visitor to visit a visitor? :trollface:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This reminds me, we can reuse this in NondeterministicResultCheckingVisitor

@maumar maumar force-pushed the fix7290 branch 4 times, most recently from 61f2b80 to f20423b Compare March 10, 2017 04:20
@tuespetre
Copy link
Contributor

@maumar I have an idea that might eliminate the need for not only the PreProcessQuerySource stuff, but also the SnapshotQuerySourceMapping/PopulateQueryModelMapping etc. stuff.

/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class RecursiveQueryModelVisitor : QueryModelVisitorBase
Copy link
Contributor

Choose a reason for hiding this comment

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

Yo @maumar would you mind splitting this into its own PR so it can be merged sooner? I've got multiple places in multiple branches where I'd like to use this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure

Copy link
Contributor

Choose a reason for hiding this comment

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

@maumar - We talked about it yesterday only. 😄 I had implemented a visitor which would apply QueryModelVisitor recursively. Lets discuss about it.

@tuespetre - PR #7822 can utilize such recursive visitor & detect all subquery expressions but there is one test which is regressing on that due to how parsed querymodel is generated and order which optimizations are applied. If you want to tackle that issue in that way. I do not want you to block you on that so the current changes in that PR is also fine.

Copy link
Contributor

Choose a reason for hiding this comment

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

#7843 is merged so you can simply this. Can you also post reference where this is being used? (I suppose there is derived class for this else this 2 visitor does nothing other than calling each other. So much Love)

@@ -129,6 +137,21 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
return base.VisitMethodCall(node);
}

protected override Expression VisitExtension(Expression node)
Copy link
Contributor

Choose a reason for hiding this comment

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

Base VisitExtension calls VisitChildren on the extension node. I know we have been inconsistent with this but I'd like to fix that 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we skip over NullConditionalExpression in ExpressionVisitorBase. However we do process NullConditional in DefaultQueryExpressionVisitor so it can be safely removed here

Copy link
Contributor

Choose a reason for hiding this comment

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

we skip over NullConditionalExpression in ExpressionVisitorBase

Yeah, this needs to change, too. We are breaking the pattern by doing that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I will do this in a separate PR

propertyExpression,
constantNull);
return addNullCheck
? new NullConditionalExpression(target, target, propertyExpression)
Copy link
Contributor

Choose a reason for hiding this comment

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

For this reason, I wonder what is difference between caller and nullableCaller in NullConditionalExpression.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

its relevant for the nested NCEs - nullable caller can be another NCE, whereas caller and access operation will always be normal expressions (methods/member access)

private void MaterializeOptionalNavigationSource(QuerySourceReferenceExpression sourceQsre)
{
if (sourceQsre != null
&& sourceQsre.ReferencedQuerySource is AdditionalFromClause additionalFromClause
Copy link
Contributor

Choose a reason for hiding this comment

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

DRY this conditions with the GroupJoin visitor. You can put it in QuerySourceExtensions if it is used outside of this file. Or private function in this file. This thing basically takes AdditionalFromClause and find the pattern.

Copy link
Contributor

Choose a reason for hiding this comment

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

PreProcessQuerySource uses this. Make extension method on AdditionalFromClause. Can also used in GroupJoin visitor if you override VisitAdditionalFromClause method.
Name this pattern something good in extension method like IsMaumerJoin :trollface:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wrt GroupJoinMaterializationCompensatingVisitor I need to process GroupJoin clauses, not AdditionalFrom clauses because there can be a groupjoin without select many, and we would not catch those cases (i.e. wouldn't mark them for materialization)

public override void Optional_navigation_inside_nested_method_call_translated_to_join_keeps_original_nullability_also_for_arguments()
{
base.Optional_navigation_inside_nested_method_call_translated_to_join_keeps_original_nullability_also_for_arguments();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

remove these.

@@ -71,8 +74,13 @@ protected override Expression VisitSubQuery(SubQueryExpression expression)

var queryModelVisitor = (RelationalQueryModelVisitor)CreateQueryModelVisitor();

var queryModelMapping = new Dictionary<QueryModel, QueryModel>();
expression.QueryModel.PopulateQueryModelMapping(queryModelMapping);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did we not require this before and what changes brought this requirement now?

Copy link
Contributor Author

@maumar maumar Mar 11, 2017

Choose a reason for hiding this comment

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

It was an existing bug, we just didn't catch it because for the relevant tests we have, LOJ and IJ were returning the same results.

requiresMaterializationExpressionVisitor);

groupJoinMaterializationExpressionVisitor.QueryModelVisitor = groupJoinMaterializationQueryModelVistor;
groupJoinMaterializationQueryModelVistor.VisitQueryModel(queryModel);
var unboundPropertyAccessCompensatingVisitor = new UnboundPropertyAccessCompensatingVisitor(
Copy link
Contributor

Choose a reason for hiding this comment

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

This 🔥

Copy link
Contributor

@smitpatel smitpatel left a comment

Choose a reason for hiding this comment

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

Looks good.

@maumar
Copy link
Contributor Author

maumar commented Mar 11, 2017

new version up

@@ -666,7 +653,7 @@ private static Expression HandleSum(HandlerContext handlerContext)

if (!(expression.RemoveConvert() is SelectExpression))
{
var sumExpression = new SqlFunctionExpression("SUM", expression.Type, new [] { expression });
var sumExpression = new SqlFunctionExpression("SUM", handlerContext.QueryModel.SelectClause.Selector.Type, new [] { expression });
Copy link
Contributor

Choose a reason for hiding this comment

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

test failure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, problematic scenario is entities.Select(e => (int?)e.Optional.Id)).Sum() which we now fully translate to sql. When converting single result element to a collection, the expected element TResult is int? but looking at selectExpression, we think its int (because we nuke all converts when translating). Fix is to look at query model instead which has appropriate typing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Don't forget Min/Max! 🌵

Copy link
Contributor

@smitpatel smitpatel left a comment

Choose a reason for hiding this comment

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

LGTM.
Wait for @anpete to final sign off on this,

return _tables.Any(te => te.QuerySource == querySource || te.HandlesQuerySource(querySource));
var processedQuerySource = PreProcessQuerySource(querySource);

return _tables.Any(te => te.QuerySource == processedQuerySource || te.HandlesQuerySource(processedQuerySource));
Copy link
Contributor

Choose a reason for hiding this comment

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

Can be simplified (only needs to check if HandlesQuerySource)

Copy link
Contributor

Choose a reason for hiding this comment

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

Also wouldn't hurt to fallback via OrElse to base.HandlesQuerySource

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried that initially, but some queries deteriorate - CROSS JOIN gets converted to CROSS APPLY, where no correlation is present. Will leave as is.

&& subQueryExpression.QueryModel.ResultOperators.Count == 1
&& subQueryExpression.QueryModel.ResultOperators[0] is DefaultIfEmptyResultOperator
&& subQueryExpression.QueryModel.MainFromClause.FromExpression is QuerySourceReferenceExpression subqueryQsre
&& subqueryQsre.ReferencedQuerySource is GroupJoinClause groupJoinClause)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably explicitly check if the GroupJoinClause is the immediately preceding body clause in the query model

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this actually requires query model, which we don't have in some contexts. Currently we are doing this check in the caller code, will keep as is.

@maumar
Copy link
Contributor Author

maumar commented Mar 14, 2017

@anpete new version up

var inputType = expression.Type;
var outputType = expression.Type;
var inputType = handlerContext.QueryModel.SelectClause.Selector.Type;
var outputType = handlerContext.QueryModel.SelectClause.Selector.Type;
Copy link
Contributor

Choose a reason for hiding this comment

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

outputType = inputType?

var groupJoinMaterializationQueryModelVistor = new RequiresMaterializationForGroupJoinQueryModelVisitor(
groupJoinMaterializationExpressionVisitor,
_querySourcesRequiringMaterialization,
var groupJoinCompensatingVisitor = new GroupJoinMaterializationCompensatingVisitor(
Copy link
Contributor

Choose a reason for hiding this comment

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

Anyway to unify all of this inside RequiresMaterializationExpressionVisitor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I implemented them intentionally outside - RequiresMaterializationExpressionVisitor should just find query sources that are reachable from the final projection and/or ones that need to be materialized due to client evals. Those visitors here are just hacks/workarounds for existing issues and once those issues are resolved, they can be removed - without tinkering with the RMEV, which is itself quite complicated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Tracking issues to remove hacks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#7787 is already tracking OptionalCollectionNavigationCompensatingVisitor, will file a new issue to track GroupJoinMaterializationCompensatingVisitor once this is checked in (and 7290 gets closed) - to track the remaining work on groupjoin materialization

…ulting in unnecessary data pulling

Problem was that for GroupJoin we would always force materialization on participating query sources.
We were doing this because we are not always able to correctly divide outer elements of the GroupJoin into correct groups.
However, if the GroupJoin clause is wrapped around SelectMany clause the groups don't matter because they are getting flattened by SelectMany anyway.

Fix is to recognize those scenarios and only force materialization when the correct grouping actually matters.
We can avoid materialization if the GroupJoin is followed by SelectMany clause (that references the grouping) and that the grouping itself is not present anywhere else in the query.
This addresses optional navigations, which is the 80% case. Manually created GroupJoins that are not modeling LeftOuterJoins still require additional materialization, but this can be addressed later as the priority is not nearly as high.

Other bugs fixed alongside the main change:

#7722 - Query : error during compilation for queries with navigation properties and First/Single/client method operators inside a subquery.

Problem here was that for some queries we don't know how to properly bind to a value buffer (when the result of binding is subquery, and not qsre).
Fix/mitigation is to recognize those scenarios and force materialization on the subqueries. This can be properly addressed in later commit (i.e. by improving the binding logic)

#7497 - Error calling Count() after GroupJoin()

(and removed the temporary fix to #7348 and implemented a proper one)
This turned out to be a chicken-and-egg problem - we had discrepancy between query sources that we marked for materialization in case of GroupJoin + result operator, and the actual client side operation that had to be performed.
We had to fix the group join materialization to allow for translation of those result operators, but without fixing this issue also we would get invalid queries in some cases.
Proper fix is to force client result operator only if the GroupJoin cannot be flattened. We also now do this for all result operators, not just a subset because it was possible to generate invalid queries with operaors like Count().
This is now consistent with the behavior of RequiresMaterializationExpressionVisitor.

Also fixed several minor issues that were encountered once no longer do extensive materialization.
@maumar maumar merged commit 03a990c into dev Mar 15, 2017
@smitpatel smitpatel deleted the fix7290 branch March 15, 2017 03:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants