-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Adding FieldValue.increment() #2075
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
Conversation
4c0da57 to
680a7e6
Compare
680a7e6 to
113c54b
Compare
113c54b to
9b42925
Compare
var-const
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The most important comment is about detecting signed integer overflow.
| return *transformation_.get(); | ||
| } | ||
|
|
||
| const bool idempotent() const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I don't think there is ever a reason to return a const primitive. It doesn't actually affect the behavior from the point of view of the caller who may assign the return value to either a const or a non-const variable. For non-primitive types, it's also generally isn't advisable, because const would prevent moving:
std::vector<int> make_huge_vector();
const std::vector<int> make_huge_vector_const();
auto foo = make_huge_vector(); // The vector is moved into foo
auto bar = make_huge_vector_const(); // No move here -- return value optimization should help, but it's not guaranteedSee Core Guidelines for additional details:
It is not recommended to return a
constvalue. Such older advice is now obsolete; it does not add value, and it interferes with move semantics
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I didn't necessarily mean to do this. This might just have been more of a copy&paste snafu. Thanks for explaining!
|
|
||
| - (void)expectApproximateLocalAndRemoteValue:(double)expectedSum { | ||
| FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent]; | ||
| XCTAssertEqual(@(expectedSum), snap[@"sum"]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be XCTAssertEqualWithAccuracy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Fixed.
I should have probably asked you not to review these tests until I was able to run them.
| - (void)testIntegerIncrementWithExistingInteger { | ||
| [self writeInitialData:@{@"sum" : @1337}]; | ||
| [self updateDocumentRef:_docRef data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}]; | ||
| [self expectLocalAndRemoteValue:13378]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/13378/1338
| [self writeInitialData:@{@"sum" : @"overwrite"}]; | ||
| [self updateDocumentRef:_docRef | ||
| data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}]; | ||
| [self expectLocalAndRemoteValue:1337]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unrelated to this PR: why was it decided to overwrite instead of reject in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backend semantics for NUMERIC_ADD follow those of ARRA_UNION and coerce to the expected type first. This ensures consistency between all document transforms.
| [self expectApproximateLocalAndRemoteValue:14.37]; | ||
| } | ||
|
|
||
| - (void)testDoubleIncrementWithExistingInteger { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: maybe replace "double" in test name with "float", "floating point", or "real"? "DoubleIncrement" reads like "IncrementTwice", not "IncrementByADouble".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to push back on this suggestions purely out of laziness. I want these tests names to match across all platforms, and changing one name here requires me to port this change to Android and Web. Thanks for flagging this though.
| class NumericIncrementTransform : public TransformOperation { | ||
| public: | ||
| NumericIncrementTransform(FSTNumberValue* operand) | ||
| : operand_(std::move(operand)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: std::move here is effectively a no-op (see one of the comments above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| : operand_(std::move(operand)) { | ||
| } | ||
|
|
||
| ~NumericIncrementTransform() override { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: defining an empty destructor shouldn't be necessary (unless the compiler complains, in which case can you copy the warning message?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed from here and from all other TransformOperation subclasses.
| if ([previousValue isKindOfClass:[FSTIntegerValue class]] && | ||
| [operand_ isKindOfClass:[FSTIntegerValue class]]) { | ||
| int64_t sum = SafeIncrement( | ||
| (reinterpret_cast<FSTIntegerValue*>(previousValue)).internalValue, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under C++ rules, static_cast would be sufficient throughout the function (it is valid to static_cast a base class to a derived class). Unless it doesn't play well with Objective-C, static_cast is preferable -- in general, it is best to use the least powerful cast applicable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still trying to wrap my head around this, but updated this to use static_cast.
| * Implements integer addition. Overflows are resolved to LONG_MAX/LONG_MIN. | ||
| */ | ||
| int64_t SafeIncrement(int64_t x, int64_t y) const { | ||
| int64_t r = x + y; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is undefined behavior, and it's not safe to rely on any particular value of r. See this thread for some suggestions. Alternatively, a larger int type would be helpful, but unfortunately Abseil only has uint128, not int128, for the time being (I guess a workaround would be to use modulo, but it might end up harder to implement than checking).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh. Thanks for pointing this out. At least on Mac with LVM, it seems to work :)
I went with one of the suggestions from the SO post, which is actually pretty reasonable: Check for overflow/underflow before it occurs.
|
|
||
| double OperandAsDouble() const { | ||
| if ([operand_ isKindOfClass:[FSTDoubleValue class]]) { | ||
| return (reinterpret_cast<FSTDoubleValue*>(operand_)).internalValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about reinterpret_cast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
schmidt-sebastian
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your thorough review. (Most) comments addressed. The tests are passing against Hexa now.
|
|
||
| - (void)expectApproximateLocalAndRemoteValue:(double)expectedSum { | ||
| FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent]; | ||
| XCTAssertEqual(@(expectedSum), snap[@"sum"]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Fixed.
I should have probably asked you not to review these tests until I was able to run them.
| [self expectApproximateLocalAndRemoteValue:14.37]; | ||
| } | ||
|
|
||
| - (void)testDoubleIncrementWithExistingInteger { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am going to push back on this suggestions purely out of laziness. I want these tests names to match across all platforms, and changing one name here requires me to port this change to Android and Web. Thanks for flagging this though.
| [self writeInitialData:@{@"sum" : @"overwrite"}]; | ||
| [self updateDocumentRef:_docRef | ||
| data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}]; | ||
| [self expectLocalAndRemoteValue:1337]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backend semantics for NUMERIC_ADD follow those of ARRA_UNION and coerce to the expected type first. This ensures consistency between all document transforms.
| [self expectApproximateLocalAndRemoteValue:13.37]; | ||
| } | ||
|
|
||
| - (void)testMultipleDoubleIncrements { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests here test the integration with the backend, and as such am I inclined to not add too much edge case testing. I am testing the internal logic that deals with NaN and Infinity in the unit tests.
| [self updateDocumentRef:_docRef | ||
| data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.01]}]; | ||
| [self updateDocumentRef:_docRef | ||
| data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.01]}]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's correct. Fixed.
| if ([previousValue isKindOfClass:[FSTIntegerValue class]] && | ||
| [operand_ isKindOfClass:[FSTIntegerValue class]]) { | ||
| int64_t sum = SafeIncrement( | ||
| (reinterpret_cast<FSTIntegerValue*>(previousValue)).internalValue, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still trying to wrap my head around this, but updated this to use static_cast.
|
|
||
| double OperandAsDouble() const { | ||
| if ([operand_ isKindOfClass:[FSTDoubleValue class]]) { | ||
| return (reinterpret_cast<FSTDoubleValue*>(operand_)).internalValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| class NumericIncrementTransform : public TransformOperation { | ||
| public: | ||
| NumericIncrementTransform(FSTNumberValue* operand) | ||
| : operand_(std::move(operand)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
| : operand_(std::move(operand)) { | ||
| } | ||
|
|
||
| ~NumericIncrementTransform() override { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed from here and from all other TransformOperation subclasses.
| * Implements integer addition. Overflows are resolved to LONG_MAX/LONG_MIN. | ||
| */ | ||
| int64_t SafeIncrement(int64_t x, int64_t y) const { | ||
| int64_t r = x + y; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh. Thanks for pointing this out. At least on Mac with LVM, it seems to work :)
I went with one of the suggestions from the SO post, which is actually pretty reasonable: Check for overflow/underflow before it occurs.
| // NOTE: The base state should only be applied if there's some existing document to | ||
| // override, so use a Precondition of exists=true | ||
| [baseMutations addObject:[[FSTPatchMutation alloc] initWithKey:mutation.key | ||
| fieldMask:FieldMask(*fieldMask) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to std::move the mask into the copy constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That question seems somewhat nonsensical.
If you're copying you shouldn't move since the whole point of copying is to preserve the existing value.
If you want to move don't copy.
Looking at this more closely I think you want to copy, and the automatic copy you get should be sufficient:
[baseMutations addObject:[[FSTPatchMutation alloc] initWithKey:mutation.key
fieldMask:*fieldMask
value:baseValues
precondition:Precondition::Exists(true)]];```There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICS, in this case std::move won't have any effect, because fieldMask points to a const object.
std::move is essentially a cast; simplifying a little bit, it's pretty much the same as static_cast<T&&>(foo). The only thing move does is affect the overload resolution:
void Foo(const Bar& bar); // Takes const lvalue reference
void Foo(Bar&& bar); // Takes rvalue reference
Bar bar;
Foo(bar); // Chooses the first overload (const lvalue reference)
Foo(std::move(bar)); // Chooses the second overload (rvalue reference)So in this case, move could affect whether it's the copy constructor or the move constructor that gets selected. However, moving a const value results in a somewhat weird type const Foo&& (const rvalue reference), which cannot bind to a non-const rvalue argument; so when overload is being selected for const Foo&&, Foo&& is out of question, and const Foo& ends up being selected. This means that the copy constructor will be used in this case, which would have happened anyway without the move.
Note that moving an object is a commitment to never use the object again except to reassign to it; moving pretty much gives up possession of the object (at least, that's the case for non-POD objects). It's not unlike setting a pointer to null -- you can reassign a different pointer, but you can no longer access the object that the pointer was pointing to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the thorough explanation. I was worried it would create an extra copy of the FieldMask before it calls the copy-constructor, but now that I understand this better, I am no longer worried. I appreciate your explanation!
| // NOTE: The base state should only be applied if there's some existing document to | ||
| // override, so use a Precondition of exists=true | ||
| [baseMutations addObject:[[FSTPatchMutation alloc] initWithKey:mutation.key | ||
| fieldMask:FieldMask(*fieldMask) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AFAICS, in this case std::move won't have any effect, because fieldMask points to a const object.
std::move is essentially a cast; simplifying a little bit, it's pretty much the same as static_cast<T&&>(foo). The only thing move does is affect the overload resolution:
void Foo(const Bar& bar); // Takes const lvalue reference
void Foo(Bar&& bar); // Takes rvalue reference
Bar bar;
Foo(bar); // Chooses the first overload (const lvalue reference)
Foo(std::move(bar)); // Chooses the second overload (rvalue reference)So in this case, move could affect whether it's the copy constructor or the move constructor that gets selected. However, moving a const value results in a somewhat weird type const Foo&& (const rvalue reference), which cannot bind to a non-const rvalue argument; so when overload is being selected for const Foo&&, Foo&& is out of question, and const Foo& ends up being selected. This means that the copy constructor will be used in this case, which would have happened anyway without the move.
Note that moving an object is a commitment to never use the object again except to reassign to it; moving pretty much gives up possession of the object (at least, that's the case for non-POD objects). It's not unlike setting a pointer to null -- you can reassign a different pointer, but you can no longer access the object that the pointer was pointing to.
| : [FSTObjectValue objectValue]; | ||
| // NOTE: The base state should only be applied if there's some existing document to | ||
| // override, so use a Precondition of exists=true | ||
| [baseMutations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation.
| for (FSTMutation *mutation in mutations) { | ||
| FSTMaybeDocument *maybeDocument = [existingDocuments objectForKey:mutation.key]; | ||
| if (!mutation.idempotent) { | ||
| // Theoretically, we should only include non-idempotent fields in this field mask as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm not sure I understand the comment. The way I'm reading it, it seems to say that ideally, we would only include non-idempotent fields, which is sufficient to prevent flicker. However, we also include idempotent fields, which has a downside. What I don't understand is why we include all fields if it has a downside and including only non-idempotent fields is sufficient?
| // better implemented as spec tests but currently they don't support transforms. | ||
|
|
||
| - (void)testHandlesSetMutationThenTransformMutationThenTransformMutation { | ||
| if ([self isTestBaseClass]) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation!
var-const
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying the comment!
dd0303f to
b74b388
Compare
|
PR updated to use MaybeDocumentMap instead of FSTImmutableDocumentMap and to use new increment() Protobuf naming. |
|
Merged master, including the changes to make MutationQueue a C++ interface. @schmidt-sebastian PTAL and this is ready for merging. |
|
Thanks for the merge. PR looks good to me. |
I am sending this out for early review, so I can get it merged by Friday. I haven't been able to run the integration tests (written on the bus to Santa Cruz), but I would appreciate some early comments.
Port of firebase/firebase-android-sdk#105 and firebase/firebase-js-sdk#1368