# Salsa -- Persistent Caching ## Ingredient Indices Ingredient indices must be stable across databases. This is automatically enforced by the `inventory` feature, or manually with the `inventory` feature disabled: ```rust let db = DatabaseImpl { storage: salsa::Storage::builder() .ingredient::<crate::query>() .ingredient::<crate::Input>() .ingredient::<crate::Tracked<'_>>() .ingredient::<crate::Interned<'_>>() .build() }; ``` ## Serializability Queries can be marked as serializable using the `serialize` attribute argument: ```rust #[salsa::tracked(serialize)] fn query(db: &dyn Db) -> Out { // ... } ``` This requires `Out` to implement `serde::{Serialize, Deserialize}`. Structs can be marked as serializable similarly: ```rust #[salsa::tracked( serialize = serde::Serialize::serialize, deserialize = serde::Deserialize::deserialize, )] struct Input { field1: usize, field2: String } ``` ## Persistence ```rust let serialized = serde_x::to_string(&<dyn salsa::Database>::as_serialize(db))?; let mut db = DatabaseImpl::new(); db.deserialize(serde_x::Deserializer::from_str(serialized)); ``` ## Inputs We need a way to bring inputs up to date after deserialization, and ensure that the revisions are bumped if necessary: ```rust #[salsa::input] struct Input { field: usize } impl salsa::ValidateInput for Input { fn validate(self, db: &mut dyn Db) { let field = read_input(); if field != self.field(db) { self.set_field(field, db); } } } ``` ## Consistency Serialization is additive; only structs/queries that are directly marked as serializable will be persisted. There are a few cases that must be handled to make this work: ### Struct Definitions ```rust #[salsa::tracked(serialize = ...)] struct Tracked<'db> { inner: Inner<'db> } #[salsa::tracked] struct Inner<'db> { ... } ``` This example will not compile, as `Inner` was not marked with the `serialize` attribute, and so does not implement `Serialize` -- we cannot persist the ID without also persisting its associated data. This also ensures that query outputs are valid. Similarly, the input of a serializable query is required to be serializable. ### Query Dependencies ```rust #[salsa::tracked(serialize)] fn query(db: &dyn Db, input: Input) -> usize { inner(db, input).len() } #[salsa::tracked] fn inner(db: &dyn Db, input: Input) -> String { input.field(db) } ``` In the above example, the memo for `inner` is not serialized. Instead, the input dependencies are flattened such that `query` has a read dependency on `input.field`. ### Tracked/Interned/Input Struct Dependencies ```rust #[salsa::tracked(serialize)] fn query(db: &dyn Db) { let interned = Interned::new(...); let tracked = Tracked::new(...); let input = Input::new(...); let _ = tracked.field(db); let _ = input.field(db); ... } ``` The above query has a read dependency on `Interned`, `tracked.field`, and `input.field`, and an output dependency on `Tracked`. However, if `Interned`/`Tracked`/`Input` are not marked as serializable, then we can drop the dependencies without forcing the structs to be serialized. Note that if any of the struct IDs were reachable from the serialized data, i.e., as part of an intermediate query input or memo, then they are required to be serializable based on the previous requirements. ```rust #[salsa::tracked(serialize)] fn query(db: &dyn Db, input: Input) -> usize { let tracked = Tracked::new(input.field(db), db); inner(tracked) } #[salsa::tracked(serialize)] fn inner(db: &dyn Db, tracked: Tracked<'_>) -> usize { tracked.field(db) } ``` ## Query Hashing There are three ways of handling intermediate queries: 1. Serialize them as a full dependency 2. _Serialize them with a hashed value_ 3. Flatten them into the parent query Hashing the return value allows finer-grained invalidations without having to serialize the output. Instead, the output hashes are compared when the query reexecutes, and the query can be backdated if it has not changed. ```rust #[salsa::tracked(serialize)] fn query(db: &dyn Db, input: Input) -> usize { let tracked = Tracked::new(input.field(db), db); inner(tracked) } #[salsa::tracked(serialize(hash))] fn inner(db: &dyn Db, tracked: Tracked<'_>) -> usize { tracked.field(db) } ```