Skip to content

CSHARP-4627: Support Union via $unionWith in LINQ3 #1078

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

Merged
merged 1 commit into from
May 11, 2023

Conversation

sanych-sun
Copy link
Member

No description provided.

@sanych-sun sanych-sun requested a review from rstam May 8, 2023 19:52
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

Good work. I have some non-trivial refactoring suggestions based on my experience writing LINQ3 translators. Let's meet on Zoom tomorrow to discuss.

throw new ExpressionNotSupportedException(innerExpression, containerExpression, because: message);
}

public static bool TryGetCollectionInfo(this Expression innerExpression, out string collectionName, out IBsonSerializer documentSerializer)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think instead of this change we need a new extension method called Evaluate:

public static object Evaluate(this Expression expression)
{
    if (expression is ConstantExpression constantExpression)
    {
        return constantExpression.Value;
    }
    else
    {
        LambdaExpression lambda = Expression.Lambda(expression);
        Delegate fn = lambda.Compile();
        return fn.DynamicInvoke(null);
    }
}

See below for where it is used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@@ -67,6 +67,8 @@ public static AstPipeline Translate(TranslationContext context, Expression expre
return TakeMethodToPipelineTranslator.Translate(context, methodCallExpression);
case "Where":
return WhereMethodToPipelineTranslator.Translate(context, methodCallExpression);
case "Union":
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: place in alphabetical order

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

internal static class UnionMethodToPipelineTranslator
{
// public static methods
public static AstPipeline Translate(TranslationContext context, MethodCallExpression expression)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would propose the following refactoring:

public static AstPipeline Translate(TranslationContext context, MethodCallExpression expression)
{
    var method = expression.Method;
    var arguments = expression.Arguments;

    if (method.Is(QueryableMethod.Union))
    {
        var firstExpression = arguments[0];
        var pipeline = ExpressionToPipelineTranslator.Translate(context, firstExpression);

        var secondExpression = arguments[1];
        var secondValue = secondExpression.Evaluate();
        if (secondValue is IMongoQueryable secondQueryable)
        {
            var secondProvider = secondQueryable.Provider;
            var secondCollectionName = secondProvider.CollectionNamespace.CollectionName;
            var secondPipelineInputSerializer = secondProvider.PipelineInputSerializer;
            var secondContext = TranslationContext.Create(secondQueryable.Expression, secondPipelineInputSerializer);
            var secondPipeline = ExpressionToPipelineTranslator.Translate(secondContext, secondQueryable.Expression);
            if (secondPipeline.Stages.Count == 0)
            {
                secondPipeline = null;
            }

            pipeline = pipeline.AddStages(
                pipeline.OutputSerializer,
                AstStage.UnionWith(secondCollectionName, secondPipeline));

            return pipeline;
        }
        else if (secondValue is IQueryable)
        {
            throw new ExpressionNotSupportedException(expression, because: "second argument must be IMongoQueryable, not just IQueryable");
        }
        else
        {
            // todo: handle enumerable constant using $documents?
        }
    }

    throw new ExpressionNotSupportedException(expression);
}

Let's discuss over Zoom tomorrow.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

.Union(_secondCollection.AsQueryable());

var stages = Translate(_firstCollection, queryable);
AssertStages(stages, "{ $unionWith : { coll : 'partners', pipeline : [] } }");
Copy link
Contributor

Choose a reason for hiding this comment

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

AssertStages(stages, "{ $unionWith : 'partners' }");

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

var stages = Translate(_firstCollection, queryable);
AssertStages(stages, "{ $unionWith : { coll : 'partners', pipeline : [] } }");

var items = queryable.ToList();
Copy link
Contributor

Choose a reason for hiding this comment

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

We normally name the results variable results (same for other test methods).

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

.Union(_firstCollection.AsQueryable());

var stages = Translate(_firstCollection, queryable);
AssertStages(stages, "{ $unionWith : { coll : 'clients', pipeline : [] } }");
Copy link
Contributor

Choose a reason for hiding this comment

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

AssertStages(stages, "{ $unionWith : 'clients' }");

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@sanych-sun sanych-sun requested a review from rstam May 9, 2023 19:22
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

99% there

@@ -44,5 +45,19 @@ public static TValue GetConstantValue<TValue>(this Expression expression, Expres
var message = $"Expression must be a constant: {expression} in {containingExpression}.";
throw new ExpressionNotSupportedException(message);
}

public static object Evaluate(this Expression expression)
Copy link
Contributor

Choose a reason for hiding this comment

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

As a general rule we keep methods in alphabetical order.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

return pipeline;
}

throw new ExpressionNotSupportedException(expression, because: "second argument must be IMongoQueryable, not just IQueryable");
Copy link
Contributor

Choose a reason for hiding this comment

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

Slight change to exception message:

throw new ExpressionNotSupportedException(expression, because: "second argument must be IMongoQueryable");

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

new Company { Id = 5, Name = "another partner" });
}


Copy link
Contributor

Choose a reason for hiding this comment

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

Delete extra blank line.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

.AsQueryable()
.Where(c => c.Name.StartsWith("second"))
.Union(_secondCollection.AsQueryable()
.Where(c => c.Name.StartsWith("another")));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested formatting:

var queryable = _firstCollection
    .AsQueryable()
    .Where(c => c.Name.StartsWith("second"))
    .Union(_secondCollection.AsQueryable().Where(p => p.Name.StartsWith("another")));

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@sanych-sun sanych-sun requested a review from rstam May 9, 2023 21:06
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

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

LGTM

Looks great!

@sanych-sun sanych-sun merged commit 4af549c into mongodb:master May 11, 2023
@sanych-sun sanych-sun deleted the csharp4627 branch May 11, 2023 00:07
dnickless pushed a commit to dnickless/mongo-csharp-driver that referenced this pull request Aug 24, 2023
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.

2 participants