Queries
Queries are read-only functions that fetch data from the database. They are reactive - when used withuseQuery on the client, they automatically re-run when data changes.
Basic query
Query context
The first argument to every query is theQueryCtx, which provides:
Read-only database access. Use
ctx.db.get() to fetch by ID or ctx.db.query() to query multiple documents.Information about the authenticated user. Call
await ctx.auth.getUserIdentity() to get the current user’s identity, or null if not authenticated.Read-only file storage access. Use
ctx.storage.getUrl(storageId) to get a URL for a stored file.Call another query within the same read snapshot. Useful for code organization, though extracting a helper function is often simpler.
Query methods
Queries support various methods for retrieving results:Full table scan
Scan all documents in a table (use sparingly on small tables only):Index queries
Query using an index for efficient lookups:The name of the index to query (must be defined in your schema).
An optional function that builds an index range using the
IndexRangeBuilder. Describes which documents to consider.Query that yields documents in index order.
Search index queries
Perform full-text search against a search index:Ordering and filtering
Order
Define the order of query results:The order to return results. Use
"asc" for ascending (default) or "desc" for descending.Filter
Filter query output with a predicate:.withIndex() over .filter() whenever possible. Filters scan all documents matched so far and discard non-matches, while indexes efficiently skip non-matching documents.
Consuming results
Queries are lazily evaluated. No work is done until you consume the results:collect
Return all results as an array:take
Return the firstn results:
The number of items to take.
first
Return the first result or null:null if the query returned no results.
unique
Return the singular result, throwing if there’s more than one:null if none exists.
Throws: An error if the query returns more than one result.
Async iteration
Process large result sets without loading all into memory:Pagination
Load pages of results with a cursor:An object containing
numItems (number of items to load) and cursor (where to start from).PaginationResult containing the page of results and a cursor to continue paginating.
Mutations
Mutations are read-write functions that modify database state. All operations within a mutation execute atomically - they either all succeed or all fail together.Basic mutation
Mutation context
The first argument to every mutation is theMutationCtx, which provides:
Read and write database access. Use
insert(), patch(), replace(), and delete() to modify data.Information about the authenticated user.
File storage with read and write access. Generate upload URLs, get file URLs, or delete files.
Schedule mutations or actions to run in the future using
runAfter() or runAt().Call a query within the same transaction. The query sees a consistent snapshot of the database.
Call a mutation within a sub-transaction. If it throws, its writes are rolled back.
Running queries and mutations
You can call other queries and mutations from within a mutation:runQuery and runMutation incur overhead of running argument and return value validation, and creating a new isolated JS context.
Validation
Both queries and mutations require argument validators using thev object:
v.string(),v.number(),v.boolean(),v.null()v.id("tableName")- table IDsv.array(validator)- arraysv.object({ ... })- objects with specific fieldsv.optional(validator)- optional fieldsv.union(v1, v2, ...)- union typesv.any()- any value (use sparingly)
Function visibility
Functions can be public or internal:Best practices
- Prefer indexes over filters - Indexes are much more efficient than scanning and filtering.
- Keep mutations focused - Each mutation should do one logical operation atomically.
- Use helper functions - Extract shared logic instead of calling
runQuery/runMutationwhen possible. - Validate inputs - Always use validators for type safety and runtime validation.
- Avoid unbounded queries - Use
.take(),.first(), or pagination instead of.collect()on queries that could grow large.