Jeff C
Jeff C•4w ago

Another call to this mutation changed the document

I encountered this error and find it very confusing. I have some parallel actions going that should be updating totally separate batches of data, but still getting this error randomly. I made a minimal reproduction that should have no change of mutual edits, but still consistently fails with the error above... Can someone please tell me if I'm missing some fundamental concept of the db operations here?
import { v } from 'convex/values'
import { internal } from './_generated/api'
import { action, internalMutation } from './_generated/server'

/**
* Updates the table values for a given groupId
*/
export const processGroup = internalMutation({
args: { groupId: v.string() },
handler: async (ctx, { groupId }) => {
// Get all existing docs with this groupId
const existingDocs = await ctx.db
.query('testDocs')
.filter((q) => q.eq(q.field('groupId'), groupId))
.collect()

for (const existingDoc of existingDocs) {
await ctx.db.delete(existingDoc._id)
}

for (let i = 0; i < 10; i++) {
await ctx.db.insert('testDocs', { groupId, value: i.toString() })
}
}
})

export const trigger = action({
handler: async (ctx) => {
const groupIds: string[] = []
for (let i = 0; i < 10; i++) {
groupIds.push(i.toString())
}
await Promise.all(
groupIds.map(async (groupId) => {
await ctx.runMutation(internal.dbTest.processGroup, { groupId })
})
)
}
})
import { v } from 'convex/values'
import { internal } from './_generated/api'
import { action, internalMutation } from './_generated/server'

/**
* Updates the table values for a given groupId
*/
export const processGroup = internalMutation({
args: { groupId: v.string() },
handler: async (ctx, { groupId }) => {
// Get all existing docs with this groupId
const existingDocs = await ctx.db
.query('testDocs')
.filter((q) => q.eq(q.field('groupId'), groupId))
.collect()

for (const existingDoc of existingDocs) {
await ctx.db.delete(existingDoc._id)
}

for (let i = 0; i < 10; i++) {
await ctx.db.insert('testDocs', { groupId, value: i.toString() })
}
}
})

export const trigger = action({
handler: async (ctx) => {
const groupIds: string[] = []
for (let i = 0; i < 10; i++) {
groupIds.push(i.toString())
}
await Promise.all(
groupIds.map(async (groupId) => {
await ctx.runMutation(internal.dbTest.processGroup, { groupId })
})
)
}
})
7 Replies
Convex Bot
Convex Bot•4w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
jamwt
jamwt•4w ago
I see it.. sorry that's confusing .filter vs .withIndex when you look up the group by groupId
const existingDocs = await ctx.db
.query('testDocs')
.filter((q) => q.eq(q.field('groupId'), groupId))
.collect()
const existingDocs = await ctx.db
.query('testDocs')
.filter((q) => q.eq(q.field('groupId'), groupId))
.collect()
that .filter is reading the whole table and filtering in memory, even though it doesn't actually care about docs not related to this group that will make it slow as well. switch to an index there and you're good
jamwt
jamwt•4w ago
Indexes | Convex Developer Hub
Indexes are a data structure that allow you to speed up your
jamwt
jamwt•4w ago
if you're in cursor, you can just highlight that line and ask it to switch it to using an index, and cursor should take care of it, including the schema.ts changes
Jeff C
Jeff COP•4w ago
ahhh you are wonderful thank you! Coming from sql land I was thinking of index as purely a speed up, but I see how it has a functional different here. Thanks for the quick assist! 🫡
jamwt
jamwt•4w ago
no problem. and yep, as you get deeper, that mental model is useful: the "conflict set" for mutations is all "index ranges" that were used in queries while it ran so if you read everything from the table, the runtime pessimistically assumed you cared about all of it. maybe you summarized it, or counted it, or something if you use an index to grab very precise sets of records, the runtime knows only those records were observed, so changes to other records can't have affected the outcome of this mutation good luck with the project!
Jeff C
Jeff COP•4w ago
Thanks for the insight!

Did you find this page helpful?