#patch only if value is undefined
17 messages · Page 1 of 1 (latest)
Hi @foggy nexus. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
Currently, Convex does not provide a built-in way to conditionally patch a field only if its value is undefined or null, without first reading the document. The typical approach is to fetch the document, check the field in your code, and then decide whether to patch it—just as you described. This does indeed consume a read from your bandwidth quota.
There is no mention in the knowledge sources of a server-side conditional patch or an atomic "patch-if-undefined" operation. All examples and documentation show that you must perform the logic in your mutation handler, which requires reading the document first, then patching if your condition is met. For example, the migration helpers and patch examples all fetch the document before deciding to patch it, as in:
if (doc.optionalField === undefined) {
await ctx.db.patch(doc._id, { optionalField: "default" });
}
This means your current approach is the standard pattern in Convex. If you want to avoid unnecessary reads, you would need to track this logic elsewhere or accept the read as part of the operation. There is no documented way to avoid the read in the current Convex API.
If you want to suggest this as a feature, you could raise it with the Convex team, as it is a common pattern in other databases (e.g., MongoDB's $setOnInsert or conditional updates).
Summary:
No, there is currently no way in Convex to patch a record only if a value is undefined or null without first reading the document. The read-then-patch pattern is required as per the current documentation and examples.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@foggy nexus Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@velvet glacier why is function using so much bandwidth.
In prod it says my bandwidth for createJobRun
Writes: 8.14 MB
Reads: 175.12 MB
It seems odd that reads is sooo much higher than writes for this function:
export const createJobRun = serverApiMutation({
args: v.object({
jobRun: v.object(jobRun),
}),
handler: async (ctx, args) => {
const job = await ctx.db
.query('job')
.withIndex('by_id', (q) => q.eq('_id', args.jobRun.jobId))
.unique();
if (!job) {
throw new ConvexError('Job not found');
}
console.debug('Job found, create job run:', args.jobRun);
await ctx.db.insert('jobRun', {
...args.jobRun,
crawlType: args.jobRun.crawlType ?? 'fetch',
crawlDuration: args.jobRun.crawlDuration ?? 0,
});
console.debug('Job run created:', args.jobRun);
// UPDATE JOB WITH LATEST RUN INFO
const jobPatch: Partial<Job> = {
status: args.jobRun?.status === 'error' ? 'failing' : 'active',
crawlType: args.jobRun?.crawlType ?? job.crawlType,
};
// Only update job values if they are not already set
if ((!job.name || job.name === job.url) && args.jobRun?.name) {
jobPatch.name = args.jobRun.name;
}
if (!job.image && args.jobRun?.image) {
jobPatch.image = args.jobRun.image;
}
if (!job.initialPrice && !(args.jobRun?.price === undefined || args.jobRun?.price === null)) {
jobPatch.initialPrice = parsePrice(args.jobRun.price);
}
console.debug('Update job:', job._id, jobPatch);
await ctx.db.patch(job._id, jobPatch);
},
});
Let me check the knowledge base for you again. One moment! :books:
The high read bandwidth for your createJobRun function is likely due to how Convex counts reads and how queries are executed. Even though your code only appears to fetch a single job document, there are a few important factors to consider:
-
Query Scanning and Index Usage:
If your index ("by_id") is not selective or not properly set up, Convex may need to scan a large number of documents to find the one matching your query. This can result in a high number of documents being read (and thus high bandwidth), even if you only use one result. It's crucial to ensure your index is correctly defined and that your query is as selective as possible. Using filters instead of indexes can also cause Convex to scan the entire table, which is very bandwidth-intensiveUsing a schema and indexes to hyper optimize queries. -
Document Size:
If the job document (or related documents) is large, even a single read can consume significant bandwidth. However, the disproportionate ratio suggests the issue is more likely with the number of documents scanned rather than their size. -
Convex Read Counting:
Convex counts all documents scanned during a query, not just the ones returned. For example, if your query scans 10,000 documents to find one match, all 10,000 are counted towards your read bandwidthRead/write limit errors. -
Debugging Bandwidth Usage:
You can use the Convex dashboard (under "Functions breakdown by project" and "Database Bandwidth") to see which functions are consuming the most bandwidth and investigate furtherDatabase bandwidth.
What to do:
- Double-check that your "by_id" index is set up correctly and is being used as intended.
- Avoid using .filter or unindexed queries, as these scan the whole table.
- Consider logging the number of documents scanned in your query, or use the Convex dashboard's function execution logs to see the actual read counts and bandwidth per callLog Streams: function_execution events.
Summary:
The most likely cause is that your query is scanning many documents due to an unselective or missing index, or because of how the query is structured. This results in high read bandwidth, even if you only use a single document in your logic. Optimizing your indexes and queries should help reduce bandwidth usage.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@foggy nexus Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@velvet glacier this is one event from createJobRun
Action Compute0.0000000 GB-hr (0 MB for 0.00s)
Database BandwidthAccessed 923 documents, 645.86 KB read, 0 B written
File Bandwidth0 B read, 0 B written
Vector Bandwidth0 B read, 0 B written
Return Value Size 16 B returned
That seems to suggest it reads all documents, but I thought by_id was default index that convex sets up
also see a lot of these errors in createJobRun
server:createJobRun
Documents read from or written to the "btreeNode" table changed while this mutation was being run and on every subsequent retry. Another call to this mutation changed the document with ID "j977pw8bbdn6vhv77d8qmj7vqh7pvy44". See https://docs.convex.dev/error#1
Let me check the knowledge base for you again. One moment! :books:
You're correct that Convex automatically creates a by_id index for each table, and this index is reserved and used for efficient lookups by document ID. However, in your code, you are using:
.withIndex('by_id', (q) => q.eq('_id', args.jobRun.jobId))
If args.jobRun.jobId is a Convex document ID, the most efficient way to fetch a single document is to use ctx.db.get(id) instead of a query with an index. Using .withIndex('by_id', ...) with a range expression that matches only one document should be efficient, but if the index is not being used as expected, or if the query is not selective enough, Convex may still scan more documents than necessary.
The log you shared:
Accessed 923 documents, 645.86 KB read
suggests that your query is scanning 923 documents to find the one you want, which is not expected for a by_id lookup. This can happen if:
- The index is not being used as intended, or
- The query is not as selective as you think (for example, if _id is not the primary key or if the value is not matching exactly).
About the error:
Documents read from or written to the "btreeNode" table changed while this mutation was being run and on every subsequent retry...
This error is related to Convex's optimistic concurrency control. If your mutation reads a large set of documents (e.g., by scanning a whole table or index range), Convex considers the entire set as "observed." If any document in that set changes during the mutation, Convex will retry, and if it keeps changing, you'll get this error. This is more likely to happen if your query is scanning many documents instead of just the one you needErrors and Warnings, Discord support thread: Another call to this mutation changed the document.
What you should do:
-
If you are fetching by document ID, use:
const job = await ctx.db.get(args.jobRun.jobId);This will only read the single document and is the most bandwidth-efficient way.
-
If you must use a query, ensure your index and query are as selective as possible. But for document ID lookups, .get is preferred.
-
Avoid using .filter or unindexed queries for this use case, as they will scan the entire table or index range, leading to high bandwidth and potential concurrency errors.
Summary:
Even though by_id is a default index, using .get is the most efficient way to fetch a document by ID. Your current approach is likely causing Convex to scan many documents, leading to high bandwidth usage and concurrency errors. Switch to ctx.db.get for best results.
If you continue to see high document scans, double-check that the value you are querying matches the document IDs exactly, and that you are not accidentally querying a non-unique field or using a less selective index.
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@foggy nexus Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@velvet glacier does ctx.db.get still work with args pass from client that are only a string
Let me check the knowledge base for you again. One moment! :books:
If you pass a plain string from the client as an argument to a Convex function that expects an Id type (e.g., for use with ctx.db.get), you will encounter a type error in TypeScript, because Convex distinguishes between strings and Id types at compile time. However, at runtime, IDs are just strings, so you can cast a string to the appropriate Id type on the client side:
const task = useQuery(api.tasks.getTask, { taskId: id as Id<"tasks"> });
On the server side, if your function argument is validated with v.id("tableName"), Convex will check that the string is a valid ID for that table. If you pass an invalid string, the validator will throw an error before ctx.db.get is called. If the string is a valid ID but does not correspond to an existing document, ctx.db.get will return nullDocument IDs, Discord: Getting 1 item by _ID from the DB., Help understanding 'Validator error: Expected ID for table...'.
Summary:
- If you want to use ctx.db.get, you should cast your string to the correct Id type on the client.
- The server will validate the ID using v.id("tableName") if you use it in your args.
- If the string is not a valid ID for the table, the validator will throw an error.
- If it is a valid ID but not found, ctx.db.get will return null.