Skip to content

Querying

Document-store renders the latest state of each document into your database. You query the database directly using its native capabilities.

How rendering works

When you add() or edit() a document (or one syncs in from a peer), the store computes the new rendered state and hands it to your render handler, which writes it into your database — see Rendering. The table layout is whatever your handler made: real columns you designed, or a generic <type>(hash, content) table if you used a provided default.

Query your database directly

Use your database's native query tools. You know your query patterns — use the right indexes, joins, and features for your use case.

SurrealDB (the provided handler's layout)

typescript
// db = your own Surreal connection (the one you gave createSurrealRenderHandler)

// All bookmarks, newest first
const [bookmarks] = await db.query(
    'SELECT doc FROM bookmark ORDER BY doc.updatedAt DESC'
);

// Filter by field
const [matches] = await db.query(
    'SELECT doc FROM bookmark WHERE doc.url = $url',
    { url: 'https://example.com' }
);

// Relations and graph traversal
const [tree] = await db.query(
    'SELECT doc, ->contains->bookmark.doc AS bookmarks FROM folder WHERE hash = $hash',
    { hash: folderHashAsArrayBuffer }
);

Create indexes for your query patterns:

surql
DEFINE INDEX updated_idx ON bookmark FIELDS doc.updatedAt;
DEFINE INDEX uid_idx ON bookmark FIELDS doc.uid;
DEFINE INDEX title_search ON bookmark FIELDS doc.title SEARCH ANALYZER simple BM25;

SQLite (your own table layout)

If your handler projects fields into real columns (the recommended pattern), queries are plain SQL:

typescript
const bookmarks = db.prepare(
    'SELECT * FROM bookmark ORDER BY updated DESC'
).all();

const matches = db.prepare(
    'SELECT * FROM bookmark WHERE url LIKE ?'
).all('%example.com%');

Anything else

Postgres, DuckDB, an in-memory map — if your render handler writes it, your reads query it. The store doesn't care.

Rendered document format

Each rendered document contains:

  • All your app fields (url, title, etc.)
  • hash, uid, parent, share, write — system fields, always present when set
  • createdAt, updatedAt — timestamps, always present (rename in your render handler if your tables want other names)
typescript
store.registerType('bookmark');

// Rendered content (what your render handler receives) looks like:
{
    hash: Buffer,         // always
    createdAt: 1710000000,
    updatedAt: 1710001000,
    uid: Buffer,
    url: 'https://...',
    title: 'Example',
}

Why not a query abstraction?

Databases have fundamentally different query capabilities — indexes, full-text search, aggregation pipelines, JSON operators, geospatial queries. A wrapper that covers all of that across backends would be a massive surface area to maintain, and would always be a worse version of the real thing.

The render-handler contract for writes is simple — four operation types. That's easy to keep portable. Reads are where databases diverge, and where your app needs full control.