Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Choosing query boundaries

A query is a reusable question, not a requirement to wrap every helper function. Start with work that is expensive, reused, or useful as a boundary where equality can stop changes. Keep ordinary local computation inside it when no caller benefits from retaining the intermediate answer.

For our analyzer, a words query is useful because many text edits preserve the word vector. A query for every byte would add keys, records, and checks while doing almost no work per call.

Keys identify questions; reads identify observations

Use a stable key such as a file identity or an owner identity. Read changing content through inputs or queries. Putting the entire current source text or revision in the key turns each edit into a new question; Ajika cannot find the old answer to compare.

Context belongs in the key when it changes the question. If one file is analyzed under two language editions, a query keyed only by the file would conflate them. Include the relevant interpretation context, or read it through an input keyed in a way that identifies that interpretation.

Keys are owned and implement Clone + Eq + Hash + Send + Sync + 'static + Persist. The context is the first argument; every argument after it contributes to the key. Values are Send + Sync + 'static; persistent values also need Persist + PartialEq. These are the ordinary Query bounds, whether the implementation is handwritten or generated.

Return a value or reuse an Arc

Declare a query with Result<T> and return an owned T. The macro wraps successful results in Arc<T>, so the generated function returns Result<Arc<T>> to callers.

Sometimes the answer already belongs to an Arc: another query, a captured input, or a named output may have produced it. Use forward to return that Arc directly, without copying the payload or allocating another wrapper. The word-count example includes a query for the fixed notes.txt path:

extern crate ajika;
extern crate anyhow;
use std::{sync::Arc, time::Duration};

use ajika::{Database, Db, Inputs, Retention};

#[ajika::input(key = String)]
struct File {
    #[input(arc)]
    text: String,
}

#[ajika::query]
fn words(db: &Db<'_>, path: String) -> ajika::Result<Vec<String>> {
    let text = File::at(path).text(db)?;
    Ok(text.split_whitespace().map(str::to_owned).collect())
}

#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
    Ok(words(db, path)?.len())
}

// This convenience query reuses the words query's allocation.
#[ajika::query(forward)]
fn note_words(db: &Db<'_>) -> ajika::Result<Arc<Vec<String>>> {
    words(db, "notes.txt".into())
}

fn main() -> anyhow::Result<()> {
    let limits = Retention {
        max_entries: 10_000,
        max_bytes: 256 * 1024 * 1024,
        max_idle: Duration::from_secs(24 * 60 * 60),
    };
    let mut database = Database::memory(Inputs::default(), (), limits);
    let file = File::at("notes.txt".into());
    database.update_inputs(|update| file.set_text(update, "hello world".to_owned()))?;

    let count = database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(*count, 2);
    assert_eq!(database.statistics().executed, 2);

    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 0);

    database.update_inputs(|update| file.set_text(update, "hello    world".to_owned()))?;
    database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(database.statistics().executed, 1); // Only words ran.

    database.update_inputs(|update| file.set_text(update, "hello new world".to_owned()))?;
    let count = database.run(|db| word_count(db, "notes.txt".into()))?;
    assert_eq!(*count, 3);
    assert_eq!(database.statistics().executed, 2);
    database.run(|db| {
        let words = words(db, "notes.txt".into())?;
        assert!(Arc::ptr_eq(&words, &note_words(db)?));
        Ok(())
    })?;
    Ok(())
}

Both words and note_words return Result<Arc<Vec<String>>> to their callers. note_words reuses the allocation returned by words.

The forward option works on associated queries too.

Borrow the answer for an operation

Use returns(ref) when callers only need to read the answer during the operation. The body still returns Result<T>, and the generated function returns Result<&'db T>.

extern crate ajika;
use ajika::{Db, Result};

#[ajika::query(returns(ref))]
fn label(_db: &Db<'_>, id: u32) -> Result<String> {
    Ok(format!("file-{id}"))
}

fn label_text<'db>(db: &Db<'db>, id: u32) -> Result<&'db str> {
    Ok(label(db, id)?.as_str())
}

returns(arc) spells out the default for callers that need an owned answer. Both modes work on free and associated queries and can be combined with forward.

Call a query explicitly

A free #[ajika::query] function also generates a query type with the same name. Pass that type to db.query::<Q>(key) for an Arc or to db.query_ref::<Q>(key) for a borrow. Both calls use the same cached answer and track the same dependency. They let you choose the return mode at the call site.

extern crate ajika;
use std::sync::Arc;
use ajika::{Db, Result};

#[ajika::query(returns(ref))]
fn location(_db: &Db<'_>, file: u32, line: u32) -> Result<String> {
    Ok(format!("{file}:{line}"))
}

fn keep_location(db: &Db<'_>) -> Result<Arc<String>> {
    db.query::<location>((4, 12))
}

fn borrow_location<'db>(db: &Db<'db>) -> Result<&'db String> {
    db.query_ref::<location>((4, 12))
}

Use () for a query with no key arguments, the argument itself for one, and a tuple in argument order for several. For a handwritten query, use the type that implements Query and pass its Query::Key. For associated queries, call the generated associated function with its declared return mode.

Put a query beside a natural owner

An associated query can make a constructor-like computation easier to find:

extern crate ajika;
use ajika::{Db, Result};

struct WordIndex(Vec<String>);

#[ajika::query]
impl WordIndex {
    #[ajika::query(transient)]
    fn from_text(_db: &Db<'_>, text: String) -> Result<Self> {
        Ok(Self(text.split_whitespace().map(str::to_owned).collect()))
    }

    fn len(&self) -> usize {
        self.0.len()
    }
}

This small example takes text directly as an owned key to demonstrate the signature. A file-backed index would normally key by the file and read its text through an input.

Mark the concrete inherent impl and each query function. Ordinary methods remain ordinary methods, and Self works in signatures and bodies. Query functions have no receiver. Generic and trait impls are not supported.

The owner also becomes part of the query’s default name, so WordIndex::from_text gets a name such as module::WordIndex::from_text. The query attribute documentation lists the available overrides and other declaration options.

Choose what survives an operation

#[ajika::query] retains its result according to the database backend. #[ajika::query(transient)] releases the primary result when the operation ends. The latter is useful for an intermediate that should not need serialization or equality.

A transient query’s dependency record can still be retained. A stored parent may validate through it without asking for its value. If someone does need that value again, its function runs to reconstruct it.

When the old dependencies are still valid, that reconstruction can preserve the known output version. If its dependencies changed, there is no PartialEq comparison available for the transient result, so Ajika conservatively assigns a fresh version. Use a persistent semantic result when equality is an important invalidation boundary.

DecisionControls
Persistent or transient queryWhether its primary payload can be retained.
Input LifetimeHow long work derived from that observation may be retained.
Memory or disk databaseHow eligible persistent-policy results are stored.

A memory database does not make transient query values survive operations. A serializable result depending on session input does not become durable.

Equality is part of the contract

After recomputation, PartialEq decides whether consumers may keep using the previous answer. It must include every property they can observe. Ajika may retain the old serialized bytes when values compare equal.

If locations should change without invalidating semantic consumers, split them into outputs; do not hide observable locations from equality. Likewise, answer-affecting reads through services or mutable global state would break determinism. Host services are for facilities such as progress reporting, not an alternative input channel.

Keep output granularity proportional to how callers read data. Reading an Output<T> decodes the whole T, not selected fields. An operation also retains its touched results until it finishes. A finer query boundary can reduce the data requested; it does not eliminate the cost of a large operation that requests everything.