const doc = await ctx.db.get(id); // Document | null
if (!doc) throw new Error('Not found');const id = await ctx.db.insert('tableName', {
field1: 'value',
field2: 123,
});
// Returns: Id<'tableName'>// Patch: partial update
await ctx.db.patch(id, { field1: 'newValue' });
// Replace: complete replacement
await ctx.db.replace(id, {
field1: 'value',
field2: 456,
});await ctx.db.delete(id);// All documents
const docs = await ctx.db.query('tableName').collect();
// First N
const docs = await ctx.db.query('tableName').take(10);
// First one
const doc = await ctx.db.query('tableName').first(); // Doc | null
// Count
const count = await ctx.db.query('tableName').count();// Default: ascending by _creationTime
const docs = await ctx.db.query('tableName').collect();
// Explicit order
const docs = await ctx.db.query('tableName').order('desc').collect();
const docs = await ctx.db.query('tableName').order('asc').collect();// Simple filter
const docs = await ctx.db.query('tableName')
.filter(q => q.gt(q.field('age'), 18))
.collect();
// Multiple conditions with and/or
const docs = await ctx.db.query('tableName')
.filter(q => q.and(
q.gte(q.field('price'), 10),
q.lte(q.field('price'), 100)
))
.collect();
const docs = await ctx.db.query('tableName')
.filter(q => q.or(
q.eq(q.field('status'), 'active'),
q.eq(q.field('status'), 'pending')
))
.collect();
// Nested field access
const docs = await ctx.db.query('users')
.filter(q => q.eq(q.field('profile.verified'), true))
.collect();// Comparison
q.eq(left, right) // ==
q.neq(left, right) // !=
q.lt(left, right) // <
q.lte(left, right) // <=
q.gt(left, right) // >
q.gte(left, right) // >=
// Logical
q.and(left, right)
q.or(left, right)
q.not(expr)
// Arithmetic
q.add(left, right)
q.sub(left, right)
q.mul(left, right)
q.div(left, right)
q.mod(left, right)
q.neg(operand)const docs = await ctx.db.query('messages')
.withIndex('by_author', q => q.eq('author', 'Alice'))
.collect();// Greater than
const docs = await ctx.db.query('messages')
.withIndex('by_time', q => q.gt('_creationTime', startTime))
.collect();
// Bounded range
const docs = await ctx.db.query('messages')
.withIndex('by_time', q =>
q.gte('_creationTime', start).lt('_creationTime', end)
)
.collect();// Schema
defineTable({ author: v.string(), timestamp: v.number() })
.index('by_author_and_time', ['author', 'timestamp'])
// Query: author exact + time range
const docs = await ctx.db.query('messages')
.withIndex('by_author_and_time', q =>
q.eq('author', 'Alice').gt('timestamp', startTime)
)
.collect();// Schema
defineTable({ title: v.string(), body: v.string(), category: v.string() })
.searchIndex('search_body', {
searchField: 'body',
filterFields: ['category'],
})
// Query
const docs = await ctx.db.query('posts')
.withSearchIndex('search_body', q =>
q.search('body', 'keyword').eq('category', 'tech')
)
.collect();// Backend
import { paginationOptsValidator } from 'convex/server';
export const list = query({
args: { paginationOpts: paginationOptsValidator },
handler: async (ctx, args) =>
await ctx.db.query('messages')
.order('desc')
.paginate(args.paginationOpts),
});
// Returns: { page: Doc[], isDone: boolean, continueCursor: string }
// React
import { usePaginatedQuery } from 'convex/react';
const { results, status, loadMore } = usePaginatedQuery(
api.messages.list,
{},
{ initialNumItems: 20 }
);
if (status === 'CanLoadMore') {
<button onClick={() => loadMore(20)}>Load More</button>
}// Scheduled functions
const jobs = await ctx.db.system.query('_scheduled_functions').collect();
// Storage files
const files = await ctx.db.system.query('_storage').collect();
// Get by ID
const job = await ctx.db.system.get(jobId); // jobId: Id<'_scheduled_functions'>
const file = await ctx.db.system.get(storageId); // storageId: Id<'_storage'>// Validate string is valid ID
const id = ctx.db.normalizeId('tableName', stringId);
if (!id) return null; // Invalid ID
const doc = await ctx.db.get(id);const results = [];
for await (const doc of ctx.db.query('tableName')) {
results.push(processDoc(doc));
if (results.length >= 100) break;
}const user = await ctx.db.query('users')
.withIndex('by_email', q => q.eq('email', email))
.unique(); // Throws if multiple matchesconst exists = await ctx.db.query('users')
.withIndex('by_username', q => q.eq('username', username))
.first() !== null;const ids = [id1, id2, id3];
const docs = await Promise.all(ids.map(id => ctx.db.get(id)));
// Filter nulls
const validDocs = docs.filter(d => d !== null);const doc = await ctx.db.get(id);
if (doc && doc.status === 'pending') {
await ctx.db.patch(id, { status: 'processed' });
}const existing = await ctx.db.query('users')
.withIndex('by_email', q => q.eq('email', email))
.unique();
if (existing) {
await ctx.db.patch(existing._id, { lastSeen: Date.now() });
} else {
await ctx.db.insert('users', { email, lastSeen: Date.now() });
}