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

CSHARP-5201: Allow update to supply sort option #1453

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

papafe
Copy link
Contributor

@papafe papafe commented Sep 18, 2024

No description provided.

Copy link
Contributor

@BorisDog BorisDog 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 overall! Now we need to wait for spec finalization.

@@ -342,7 +342,7 @@ private class TestCaseFactory : JsonDrivenTestCaseFactory
new[]
{
"MongoDB.Driver.Tests.Specifications.crud.tests.v1",
"MongoDB.Driver.Tests.Specifications.crud.tests.v2"
"MongoDB.Driver.Tests.Specifications.crud.tests.v2" //TODO I don't think we have v2 anymore?
Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch, we don't, can be removed.

@@ -124,8 +124,8 @@ protected override IEnumerable<JsonDrivenTestCase> CreateTestCases(BsonDocument
}

var description = testCase.Test["description"].AsString;
var lineNumer = lines.FirstOrDefault(p => p.Line.Contains(description)).Index;
testCase.Test.Add("_lineNumber", lineNumer);
var lineNumber = lines.FirstOrDefault(p => p.Line.Contains(description)).Index;
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@@ -112,6 +112,7 @@ public UnifiedUpdateOneOperation Build(string targetCollectionId, BsonDocument a
{
switch (argument.Name)
{
//TODO Why don't we create UpdateOptions beforehand?
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably to exercise options == null path.

};
//TODO Why don't UpdateOne and UpdateOneAsync have a common path for creating the UpdateOneModel and BulkWriteOptions (if not more)?
Copy link
Contributor

Choose a reason for hiding this comment

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

In that case that can be done elegantly, we should do that in a separate ticket for other methods as well. Usually we can address such improvements on green-build Fridays.

/// <summary>
/// Gets or sets the sort document.
/// </summary>
public BsonDocument Sort
Copy link
Contributor

Choose a reason for hiding this comment

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

Alphabetical order.

src/MongoDB.Driver/BulkWriteReplaceOneModel.cs Outdated Show resolved Hide resolved
src/MongoDB.Driver/BulkWriteUpdateOneModel.cs Outdated Show resolved Hide resolved
/// <summary>
/// The sort document to use.
/// </summary>
public BsonDocument Sort { get; init; }
Copy link
Member

Choose a reason for hiding this comment

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

Should we use SortDefinition<TDocument>?

@@ -31,6 +31,7 @@ public sealed class ReplaceOneModel<TDocument> : WriteModel<TDocument>
private BsonValue _hint;
private bool _isUpsert;
private readonly TDocument _replacement;
private BsonDocument _sort;
Copy link
Member

Choose a reason for hiding this comment

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

Should we use SortDefinition<TDocument>?

/// <summary>
/// Gets or sets the sort document.
/// </summary>
public BsonDocument Sort
Copy link
Member

Choose a reason for hiding this comment

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

Use SortDefinition<TDocument>.

/// <summary>
/// Gets or sets the sort document.
/// </summary>
public BsonDocument Sort
Copy link
Member

Choose a reason for hiding this comment

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

Use SortDefinition<TDocument>.

/// <summary>
/// Gets or sets the sort document.
/// </summary>
public BsonDocument Sort
Copy link
Member

Choose a reason for hiding this comment

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

Use SortDefinition<TDocument>.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately UpdateOptions and ReplaceOptions are not generic classes, so we can't parametrize the properties. We can add the parameter, but I think it would be a breaking change.
We could also add the generic UpdateOptions<TDocument> alongside the non generic version and make new methods that use this, while deprecating the methods that are using the non generico version. This would be the long road

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 have done this only for ReplaceOptions for now. If all I've done makes sense, I'll do the same for UpdateOptions.
This required to create a new class ReplaceOptions<T> and an implicit conversion operator from ReplaceOptions to ReplaceOptions<T>.

Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

We should decide if we are ready for the breaking change by replacing ReplaceOption with ReplaceOption before reviewing further changes.

@@ -200,6 +201,7 @@ public void RenderUpdateOne<TDocument>(RenderArgs<BsonDocument> renderArgs, Bson
WriteArrayFilters(serializationContext, model.ArrayFilters);
WriteHint(serializationContext, model.Hint);
WriteCollation(serializationContext, model.Collation);
WriteSort(serializationContext, model.Sort?.Render(renderArgs.WithNewDocumentType(documentSerializer)));
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to make WriteSort and pass model.Sort there? And move Render there as well.

@@ -972,8 +972,7 @@ public interface IMongoCollection<TDocument>
/// <returns>
/// The result of the replacement.
/// </returns>
ReplaceOneResult ReplaceOne(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions options = null, CancellationToken cancellationToken = default(CancellationToken));

ReplaceOneResult ReplaceOne(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions<TDocument> options = null, CancellationToken cancellationToken = default(CancellationToken));
Copy link
Member

Choose a reason for hiding this comment

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

It will be a code breaking change (client's code has to be rewritten) as far as I understood. Should we create a new overload instead? And support both ReplaceOptions and ReplaceOptions<TDocument>

Copy link
Member

Choose a reason for hiding this comment

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

@BorisDog WDYT?

Copy link
Contributor

@BorisDog BorisDog Oct 30, 2024

Choose a reason for hiding this comment

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

Yup we avoid breaking changes. We'll probably need an extension method.

Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a binary breaking change but not a source code breaking change.

There is also an implicit conversion operator from ReplaceOptions to ReplaceOptions<TDocument> that provides backward compatibility.

Copy link
Contributor Author

@papafe papafe Oct 31, 2024

Choose a reason for hiding this comment

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

Yes, I discussed this with Robert and this was an attempt to avoid creating more methods/overloads and keep source compatibility.
I was reading a little bit around online about semver in regards to binary compatibility. It seems that there is not a complete agreement on it, but overall it's recommended to increase major version with a binary-incompatible change.
Personally (irregardless of this case), I think this is a matter of deciding where do we stand on this and then take it from there.

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 prefer a solution that doesn't involve doubling (or worse) the number of overloads. Too many overloads is a bad thing.

The current approach uses an implicit conversion from ReplaceOptions to ReplaceOptions<TDocument> to preserve backward compatibility.

Another approach could be to use subclassing: ReplaceOptions<TDocument> : ReplaceOptions. Then we could leave the current overloads unchanged at the expense of some type safety. The code that uses the replaceOptions would have to test whether the passed in options are the subclass or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given what we discussed at Triage today I'll park this ticket until next when we can have a more general discussion about breaking changes in general

Copy link
Member

Choose a reason for hiding this comment

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

I have to apologize, I did not notice the implicit conversion. So this is not code breaking change.
On other hand I suppose it's better to follow Robert's suggestion with using of subclasses as clearer way to do the same thing. At least because this way we could avoid changes to public API.

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 went for Robert suggestion and made the generic options derive from the base class we have now

/// <summary>
/// Options for replacing a single document.
/// </summary>
public sealed class ReplaceOptions : ReplaceOptionsBase
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to ensure that this change is not breaking.

@@ -972,8 +972,7 @@ public interface IMongoCollection<TDocument>
/// <returns>
/// The result of the replacement.
/// </returns>
ReplaceOneResult ReplaceOne(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions options = null, CancellationToken cancellationToken = default(CancellationToken));

ReplaceOneResult ReplaceOne(FilterDefinition<TDocument> filter, TDocument replacement, ReplaceOptions<TDocument> options = null, CancellationToken cancellationToken = default(CancellationToken));
Copy link
Contributor

@BorisDog BorisDog Oct 30, 2024

Choose a reason for hiding this comment

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

Yup we avoid breaking changes. We'll probably need an extension method.

: this(CollectionNamespace.FromFullName(collectionNamespace), filter, replacement, collation, hint, isUpsert)
bool isUpsert = false,
SortDefinition<TDocument> sort = null)
: this(CollectionNamespace.FromFullName(collectionNamespace), filter, replacement, collation, hint, isUpsert, sort)
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 is not binary compatible.
The alternative would be to add other two constructors (given that we already have two) but:

  • Then we have 4 very similar constructors
  • Developers using those constructors will have errors due to constructor ambiguity if they do not specify all parameters. This is theoretically still source compatible, but definitely annoying
    Any suggestions about this? Personally I'd just keep the sort definition out of the constructor if we cannot find another alternative

Copy link
Member

Choose a reason for hiding this comment

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

What if we add new constructor with sort as the very last non-optional parameter? In such way it would not be a breaking change and there would not be an ambiguity.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good idea, I didn't think about it :)
I've added two new constructor to mirror the ones we have already, and added a new private one that is being called by all the others

Copy link
Member

Choose a reason for hiding this comment

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

Nice! And I suppose the plan should be to eliminate this copied ctors in next major release, by making the sort parameter as optional. So the question is: how we going to track this and not forget to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, that would be the plan! I've added the constructors and the non generic options to the list of breaking changes for next version.

@papafe papafe marked this pull request as ready for review November 4, 2024 14:31
@papafe papafe requested a review from a team as a code owner November 4, 2024 14:31
@@ -1132,7 +1132,7 @@ public void ReplaceOne_should_call_collection_with_expected_arguments(

assertReplaceOne();

var replaceOptions = new ReplaceOptions();
var replaceOptions = new ReplaceOptions<Person>();
Copy link
Member

Choose a reason for hiding this comment

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

Should we revert this change as we should be backward compatible now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Definitely

: this(CollectionNamespace.FromFullName(collectionNamespace), filter, replacement, collation, hint, isUpsert)
bool isUpsert = false,
SortDefinition<TDocument> sort = null)
: this(CollectionNamespace.FromFullName(collectionNamespace), filter, replacement, collation, hint, isUpsert, sort)
Copy link
Member

Choose a reason for hiding this comment

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

What if we add new constructor with sort as the very last non-optional parameter? In such way it would not be a breaking change and there would not be an ambiguity.

Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

I'm ready to LGTM the changes, but could you please rebase before that? Because in the current main we already have that apicompat script, just want to be sure we did everything right in terms of backward compatibility.

@papafe
Copy link
Contributor Author

papafe commented Nov 6, 2024

@sanych-sun just rebased on main, curious to see what will the script say 😁

@papafe
Copy link
Contributor Author

papafe commented Nov 7, 2024

@sanych-sun it seems the validate-apicompat task gave its ok 😁
Do you think this is mergeable now?

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.

4 participants