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

Calls, addresses, and registration

A normal Rust call knows which function to invoke. A dependency saved yesterday must describe that call as data, so a new process can make it again.

For words("notes.txt"), that description contains two things:

  • The query family’s stable name, such as example::words.
  • The encoded key, "notes.txt".

Together they form an Address. Inputs use the same kind of address for a field family and key.

The same question can have a new answer

The storage ID is a 32-byte hash of that address. It identifies the question, not the answer’s contents. Changing the file replaces work at the same logical address instead of creating a new historical entry for every text version.

The ordinary return value uses the call’s ID. A named output derives another ID from the producer’s ID and its encoded slot. The name "locations" on two different producers therefore identifies two different outputs.

Ajika keeps the full address in saved dependencies as well as using hashes for lookup. If the child’s record is collected, the parent still has the key needed to recompute that child.

The address identifies the question; other identifiers describe what happened when answering it:

IdentifierMeaning
Call or output IDWhere this logical answer belongs.
Input stampWhat external data a query observed.
Output versionWhich answer downstream consumers observed.
Input revisionWhen this database owner last published relevant changes.

Output versions are fresh 16-byte random tokens when equality cannot be established. They are neither content hashes nor revision counters. Random tokens avoid accidentally repeating a forgotten version after a record has been collected.

From a declaration to a callable provider

Query is the evaluator’s interface to a computation. #[ajika::query] generates a provider type implementing that trait, and leaves a calling wrapper where the function was declared. The wrapper passes the provider and owned key to the evaluator; the provider’s execute method invokes the original computation.

The body declares Result<T>, while the wrapper returns Result<Arc<T>>. The provider wraps successful values in an Arc. With forward, the body already returns Result<Arc<T>>, and the provider passes that Arc through.

One key argument is used directly; zero or multiple key arguments become a tuple. Associated queries use the same machinery. Their computation stays in the owning impl so Self and helper calls keep normal Rust meaning.

For a saved address, the runtime has only a family name and encoded key, so it also needs a way to find this provider and decode its arguments. The macro emits a registration gathered by inventory when the program links. At database construction, Registry::linked collects the query implementations for that database’s services type, alongside the input declarations.

This means replay works before the application has directly called every query function. There is no “warm up registrations by executing queries” step. Handwritten Query implementations use #[ajika::register] for the same purpose.

Replay enters the normal evaluator

When validating word_count, Ajika checks whether words still has the version the count depended on. Comparing those versions does not require the word vector itself. Registry::replay resolves the saved family name, decodes the key, and enters the same evaluator used by a typed query call.

If the child’s dependencies still match, the evaluator can establish its current version without loading that vector. Changed dependencies may require executing words first. The evaluation chapter follows both paths.

If the current program has no registration for the saved query name, Ajika cannot check that old dependency. It reruns the parent instead, letting its function collect fresh reads. An error from a registered provider fails the operation.

Two different implementations cannot share a family name within the same registry. In-process TypeId checks detect that collision; TypeId is not part of the persistent address.

Changing query code or encoded types still needs an application epoch change when stored results are no longer compatible. Registration describes how to call code; it does not fingerprint the implementation.

Interning is separate from query registration

The registry connects query names to executable code. The interner instead gives application values, such as file paths, compact IDs. An interned FileId can appear inside many query keys, each of which identifies a different question about that file.

The interner is shared for the database’s lifetime and restored before queries intern new objects. Each declaration has its own numbering space. Looking up an ID’s fields does not add a query dependency because the assignment is immutable.

See Interner for assignment ownership and the user chapter for its reset boundary. The interner must remain consistent with every saved key that contains its IDs.