Overview
Zero provides a powerful, type-safe query API for fetching data from your schema. Queries are reactive - they automatically update when the underlying data changes.
Query Builder
Use createBuilder to create a query builder for your schema:
import { createBuilder } from '@rocicorp/zero' ;
import { schema } from './schema' ;
const zql = createBuilder ( schema );
Create the query builder once at module level and reuse it throughout your application.
Basic Queries
Get All Records
// Get all users
const allUsers = zql . user ;
// Materialize the query
const view = zero . materialize ( allUsers );
Get Single Record
Use .one() to get a single record instead of an array:
// Returns a single user or undefined
const user = zql . user . where ( 'id' , 'user-123' ). one ();
.one() returns undefined if no matching record is found. It throws an error if multiple records match.
Filtering
Simple Equality Filters
// Filter by exact match
const activeUsers = zql . user . where ( 'status' , 'active' );
// Chain multiple filters (AND logic)
const adminUsers = zql . user
. where ( 'status' , 'active' )
. where ( 'role' , 'admin' );
Comparison Operators
// Greater than
const recentPosts = zql . post . where ( 'created' , '>' , Date . now () - 86400000 );
// Greater than or equal
const highPriority = zql . issue . where ( 'priority' , '>=' , 5 );
// Less than
const oldPosts = zql . post . where ( 'created' , '<' , Date . now () - 86400000 * 7 );
// Less than or equal
const lowPriority = zql . issue . where ( 'priority' , '<=' , 2 );
// Not equal
const nonDeletedUsers = zql . user . where ( 'status' , '!=' , 'deleted' );
String Matching
// LIKE pattern matching
const searchResults = zql . user . where ( 'name' , 'LIKE' , '%alice%' );
// Case-insensitive LIKE
const searchResults = zql . user . where ( 'name' , 'ILIKE' , '%alice%' );
// Escape special characters in user input
import { escapeLike } from '@rocicorp/zero' ;
const userInput = 'alice%' ;
const safePattern = `% ${ escapeLike ( userInput ) } %` ;
const results = zql . user . where ( 'name' , 'LIKE' , safePattern );
IN Queries
// Match any value in array
const specificUsers = zql . user . where ( 'id' , 'IN' , [ 'user-1' , 'user-2' , 'user-3' ]);
// NOT IN
const excludedUsers = zql . user . where ( 'status' , 'NOT IN' , [ 'deleted' , 'banned' ]);
NULL Checks
// IS NULL
const unassignedIssues = zql . issue . where ( 'assigneeID' , 'IS' , null );
// IS NOT NULL
const assignedIssues = zql . issue . where ( 'assigneeID' , 'IS NOT' , null );
Sorting
Order By Single Column
// Ascending order
const usersByName = zql . user . orderBy ( 'name' , 'asc' );
// Descending order
const recentPosts = zql . post . orderBy ( 'created' , 'desc' );
Order By Multiple Columns
// First by status, then by name
const sortedIssues = zql . issue
. orderBy ( 'status' , 'asc' )
. orderBy ( 'name' , 'asc' );
Limiting Results
// Get first 10 records
const topUsers = zql . user . limit ( 10 );
// Get first 5 recent posts
const recentPosts = zql . post
. orderBy ( 'created' , 'desc' )
. limit ( 5 );
.limit() is applied after filtering and sorting.
Relationships
Query across relationships defined in your schema:
import { relationships } from '@rocicorp/zero' ;
// Define relationships
const userRelationships = relationships ( user , ({ many }) => ({
posts: many ({
sourceField: [ 'id' ],
destField: [ 'authorID' ],
destSchema: post ,
}),
}));
const postRelationships = relationships ( post , ({ one }) => ({
author: one ({
sourceField: [ 'authorID' ],
destField: [ 'id' ],
destSchema: user ,
}),
}));
// Query with relationships
const postsWithAuthor = zql . post . related ( 'author' );
// Query user's posts
const userWithPosts = zql . user
. where ( 'id' , 'user-123' )
. one ()
. related ( 'posts' );
Advanced Queries
Combining Conditions
// Multiple filters create AND conditions
const query = zql . issue
. where ( 'status' , 'open' )
. where ( 'priority' , '>=' , 5 )
. where ( 'assigneeID' , 'IS NOT' , null );
// This is equivalent to:
// WHERE status = 'open' AND priority >= 5 AND assigneeID IS NOT NULL
Complex Filters
// Search across multiple fields
const searchUsers = ( term : string ) => {
const pattern = `% ${ escapeLike ( term ) } %` ;
return zql . user . where ( 'name' , 'ILIKE' , pattern );
};
// Date range queries
const postsInRange = zql . post
. where ( 'created' , '>=' , startDate )
. where ( 'created' , '<=' , endDate );
Custom Queries
For complex queries not supported by the query builder, define custom queries:
import { syncedQuery } from '@rocicorp/zero' ;
// Define a custom query function
const searchIssues = syncedQuery (
schema ,
async ( tx , { projectID , term } : { projectID : string ; term : string }) => {
// Use tx.run() with query builder for type safety
const issues = await tx . run (
zql . issue
. where ( 'projectID' , projectID )
. where ( 'title' , 'ILIKE' , `% ${ term } %` )
);
return issues ;
}
);
// Register custom queries
import { defineQueries } from '@rocicorp/zero' ;
const queries = defineQueries ( schema , {
searchIssues ,
});
// Use in Zero client
const zero = new Zero ({
schema ,
server: 'https://your-zero-server.com' ,
userID: 'user-123' ,
});
// Materialize custom query
const view = zero . materialize (
queries . searchIssues ({ projectID: 'proj-1' , term: 'bug' })
);
Query Result Types
Queries return data with a result type indicating the state:
Result Type Values
unknown - Query is loading, data may be partial or stale
complete - Query has fully loaded from server
error - Query encountered an error
const view = zero . materialize ( zql . user );
view . addListener (( data , resultType , error ) => {
switch ( resultType ) {
case 'unknown' :
console . log ( 'Loading...' , data ); // May have partial data
break ;
case 'complete' :
console . log ( 'Complete:' , data ); // Full data from server
break ;
case 'error' :
console . error ( 'Error:' , error ); // Query failed
break ;
}
});
Query Caching with TTL
Control how long queries remain active after the last subscriber:
// Keep query alive forever
const view = zero . materialize ( query , { ttl: 'forever' });
// Destroy immediately when no subscribers
const view = zero . materialize ( query , { ttl: 'never' });
// Keep alive for 5 minutes (in seconds)
const view = zero . materialize ( query , { ttl: '5m' });
// Keep alive for 30 seconds
const view = zero . materialize ( query , { ttl: '30s' });
// Keep alive for 60 seconds (in milliseconds)
const view = zero . materialize ( query , { ttl: 60_000 });
Use longer TTL values for frequently accessed queries to improve performance and reduce server load.
Preloading Queries
Preload queries before they’re needed:
// Start loading a query early
const view = zero . materialize ( zql . user . where ( 'id' , 'user-123' ). one (), {
ttl: '1m' ,
});
// Later, when you need the data, it's already loaded
// The view is reused if the query hash matches
Indexing
Zero automatically creates indexes for primary keys and foreign keys. For best performance:
Filter by indexed columns when possible
Add indexes on frequently queried columns
Use compound indexes for multi-column filters
For large datasets, use pagination:
const PAGE_SIZE = 50 ;
// Page 1
const page1 = zql . user
. orderBy ( 'created' , 'desc' )
. limit ( PAGE_SIZE );
// Page 2 (in practice, you'd track the last ID)
const page2 = zql . user
. where ( 'created' , '<' , lastCreated )
. orderBy ( 'created' , 'desc' )
. limit ( PAGE_SIZE );
For very large lists, consider using virtual scrolling:
import { useZeroVirtualizer } from '@rocicorp/zero-virtual/react' ;
function UserList () {
const virtualizer = useZeroVirtualizer ({
query: zql . user . orderBy ( 'name' , 'asc' ),
estimateSize : () => 50 , // Estimated row height
});
return (
< div style = {{ height : '600px' , overflow : 'auto' }} >
{ virtualizer . getVirtualItems (). map ( item => (
< div key = {item. key } data - index = {item. index } >
{ item . data . name }
</ div >
))}
</ div >
);
}
TypeScript Types
Queries are fully typed based on your schema:
const schema = createSchema ({
tables: [
table ( 'user' ). columns ({
id: string (),
name: string (),
age: number (),
}). primaryKey ( 'id' ),
],
});
const zql = createBuilder ( schema );
// TypeScript knows the return type
const users = zql . user ; // Query<'user', Schema, User[]>
const user = zql . user . one (); // Query<'user', Schema, User | undefined>
// Filters are type-checked
zql . user . where ( 'name' , 'Alice' ); // ✓ Valid
zql . user . where ( 'invalidColumn' , 'value' ); // ✗ Type error
zql . user . where ( 'age' , 'not-a-number' ); // ✗ Type error
Next Steps
Mutations Learn how to modify data with mutations
React Integration Use queries in React with useQuery