Skip to Content
Jumentix DocsPackages@jumentix/canaUsage guideQuerying and plans

Querying and plans

Cana keeps querying intentionally small: choose a table, optionally choose an index, then constrain the cursor with equals, limit, offset and direction.

Indexed task query

type Task = {
  id: string;
  title: string;
  categoryId: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
  createdAt: number;
  updatedAt: number;
};

const workTasks = await client.table<Task>('tasks').query({
  index: 'byCategory',
  equals: 'work',
  direction: 'next',
  limit: 20
});

console.log(workTasks.map((task) => task.title));

There is no expression-based filter(record => record.completed). Filtering in JavaScript forces a full read and hides performance problems. Add an index such as byCompleted when the query is part of the user workflow.

Pagination

const firstPage = await client.table<Task>('tasks').query({
  index: 'byUpdatedAt',
  direction: 'next',
  offset: 0,
  limit: 20
});

const secondPage = await client.table<Task>('tasks').query({
  index: 'byUpdatedAt',
  direction: 'next',
  offset: 20,
  limit: 20
});

console.log({
  firstPageSize: firstPage.length,
  secondPageSize: secondPage.length
});

Deep offset pagination advances the IndexedDB cursor. It does not clone skipped records into a JavaScript array, but the cursor still has to move, so the shape is O(offset + limit).

Count and explain

const openCount = await client.table<Task>('tasks').count({
  index: 'byCompleted',
  equals: false
});

const explained = await client.table<Task>('tasks').explain({
  index: 'byCategory',
  equals: 'work'
});

console.log({
  openCount,
  plan: explained.plan,
  records: explained.records
});

Complexity quick map

Query shapeComplexity modelNotes
Primary-key get(key)Common IndexedDB key lookup model: O(log n).Browser implementation owns the tree details.
Indexed equalsCommon IndexedDB index model: O(log n + matches).Result size still matters.
limit: n after cursor openO(limit).Cana does not materialize the whole store.
offset + limitO(offset + limit).Cursor movement is the cost.
Native count()One native request.Cana does not read every record into JS.

Run it here

Query + explain

Run an indexed Task query by Category and inspect the plan.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
  { id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
  { id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
  {
    id: 'task-1',
    title: 'Write the Cana tutorial',
    categoryId: 'work',
    completed: false,
    priority: 'high',
    createdAt: now,
    updatedAt: now
  },
  {
    id: 'task-2',
    title: 'Review category filters',
    categoryId: 'home',
    completed: true,
    priority: 'medium',
    createdAt: now,
    updatedAt: now + 1
  }
]);
const { records, plan } = await client.table('tasks').explain({
  index: 'byCategory',
  equals: 'work'
});
return { records, plan };

Next

Continue to transactions and change events.