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

FieldValue.increment() is not working as expected on subfields of a map #1964

Open
JamieCurnow opened this issue Dec 22, 2023 · 3 comments
Open
Assignees
Labels
api: firestore Issues related to the googleapis/nodejs-firestore API. priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@JamieCurnow
Copy link

JamieCurnow commented Dec 22, 2023

Thanks for stopping by to let us know something could be better!

PLEASE READ: If you have a support contract with Google, please create an issue in the support console instead of filing on GitHub. This will ensure a timely response.

  1. Is this a client library issue or a product issue?
    I think it's probably something that should be at-least handled by this library.

  2. Did someone already solve this?

  1. Do you have a support contract?
    Nope

If the support paths suggested above still do not result in a resolution, please provide the following details.

Environment details

  • OS: Mac
  • Node.js version: 18.16.0
  • npm version: 9.5.1
  • @google-cloud/firestore version: ^6.8.0

Steps to reproduce

The FieldValue.increment() function does not work as expected on subFields of a map value. Given this data structure of the object in the database:

{
  amount: 1,
  map: {
    amount: 1
  }
}

If I run this code:

await docRef.update({
  amount: FieldValue.increment(2),
  sub: {
    amount: FieldValue.increment(2)
  }
})

The data in the database ends up being:

{
  amount: 3,
  map: {
    amount: 2
  }
}

It would seem that incrementing using this object syntax does increment on top-level keys, but "sets" the value of subfields of maps to the exact increment value, rather than incrementing.

To me this feels like the incorrect behaviour - I would firstly expect that map.amount would be 3, OR expect the SDK to throw an error as it does when using FieldValue.delete() on a subfield of a map in this way.

The "workaround" is to use dot-notation strings as keys on the top level of the object like so:

await docRef.update({
  amount: FieldValue.increment(2),
  'sub.amount': FieldValue.increment(2)
})

This does work as expected and the data in the db ends up being:

  amount: 3,
  map: {
    amount: 3
  }
}

This issue has been observed in SO here:
https://stackoverflow.com/questions/56427582/how-to-use-fieldvalue-increment-on-the-property-of-an-object-type-document-fie

It feels like this warrants an issue and a change to how the sdk handles this to bring it in line with how it handles FieldValue.delete() on subfields of a map where for example, this code throws an error:

await docRef.update({
  amount: FieldValue.increment(2),
  sub: {
    amount: FieldValue.delete()
  }
})

Error:

Update() requires either a single JavaScript object or an alternating list of field/value pairs that can be followed by an optional precondition. Value for argument "dataOrField" is not a valid Firestore value. FieldValue.delete() must appear at the top-level and can only be used in update() or set() with {merge:true} (found in field "sub.amount")

It took a wilte to figure out what was going on here, and can be especially confusing when incrementing from 0 because it seems like the first request works, but then subsequent requests do not behave as expected. Eg:

Db is:

{
  amount: 0,
  map: {
    amount: 0
  }
}

Run:

await docRef.update({
  amount: FieldValue.increment(1),
  sub: {
    amount: FieldValue.increment(1)
  }
})

Db is now:

{
  amount: 1,
  map: {
    amount: 1
  }
}

Which seems like it works... But if you run the exact same code again:

await docRef.update({
  amount: FieldValue.increment(1),
  sub: {
    amount: FieldValue.increment(1)
  }
})

Now the data in the db ends up being incorrect:

{
  amount: 2,
  map: {
    amount: 1
  }
}
@JamieCurnow JamieCurnow added priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Dec 22, 2023
@product-auto-label product-auto-label bot added the api: firestore Issues related to the googleapis/nodejs-firestore API. label Dec 22, 2023
@MarkDuckworth MarkDuckworth self-assigned this Dec 26, 2023
@MarkDuckworth
Copy link
Contributor

@JamieCurnow,

Thanks for bringing this to our attention. I reproduced the behavior and I agree that it is confusing and the throwing behavior of FieldValue.delete() on a nested property adds to that confusion.

However, this call is technically correct. It's just stating that sub should be replaced with a new object, and then increment the starting value of sub.amount by 1.

await docRef.update({
  sub: {
    amount: FieldValue.increment(1)
  }
});

The solution in your case is to use the dot notation as you already stated.

await docRef.update({
  'sub.amount': FieldValue.increment(1)
});

I'll review this issue with the team after the holidays to see if we can reduce the confusion. The solution might be raising an error, logging a warning, or adding some additional documentation.

@JamieCurnow
Copy link
Author

Hi @MarkDuckworth, that totally makes sense why this is happening now after your explanation, so thank you so much for the reply 🙌

It's super interesting that a set() with a { merge: true } works differently than an update in this situation. I think I've seen another issue that addresses that... 🤔

In the actual code, I'm changing from a set() call to an update call because I want to fail if the document doesn't already exist. So the solution for me is to just convert all nested object changes into dot-notation strings, though in doing that, I loose type-safety.

Is there any support for a { merge: true } on an update() call in the pipeline that would help this transition in the future to bring the two methods more inline? Or is that something that is just technically not possible under the Firestore hood?

Thanks again!

@MarkDuckworth
Copy link
Contributor

I just reviewed set(..., {merge: true}) and I can see that the message sent to the backend is slightly different than when using update(...). The set+merge call is requesting that sub should be merged with object {}, and then increment the existing value of sub.amount by 1.

await docRef.set({
  sub: {
    amount: FieldValue.increment(1)
  }
},
{ merge: true });

Is there any support for a { merge: true } on an update() call in the pipeline that would help this transition in the future to bring the two methods more inline? Or is that something that is just technically not possible under the Firestore hood?

This is not currently supported in the SDK from what I can tell. We will discuss allowing this in the SDK.

So the solution for me is to just convert all nested object changes into dot-notation strings, though in doing that, I loose type-safety.

update() can enforce type safety even with dot notation, except when using indexed properties.

type MyType = {
    sub: {
        amount: number
    }
};
let update: UpdateData<MyType> = {
    'sub.amount': increment(1) // must be number | FieldValue | undefined
};
await docRef.update(update);

An alternative solution would be to use a Transaction that checks for existence with a get and then performs the set. The downside to this is that it involves two operations, a read and a write.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: firestore Issues related to the googleapis/nodejs-firestore API. priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

2 participants