Database
Continuing our dissection, the other thing which a user must define is a database, which looks something like this:
#[salsa::database(HelloWorldStorage)]
#[derive(Default)]
struct DatabaseStruct {
storage: salsa::Storage<Self>,
}
impl salsa::Database for DatabaseStruct {}
The salsa::database
procedural macro takes a list of query group
structs (like HelloWorldStorage
) and generates the following items:
- a copy of the database struct it is applied to
- a struct
__SalsaDatabaseStorage
that contains all the storage structs for each query group. Note: these are the structs full of hashmaps etc that are generaetd by the query group procdural macro, not theHelloWorldStorage
struct itself. - an impl of
HasQueryGroup<G>
for each query groupG
- an impl of
salsa::plumbing::DatabaseStorageTypes
for the database struct - an impl of
salsa::plumbing::DatabaseOps
for the database struct
Key constraint: we do not know the names of individual queries
There is one key constraint in the design here. None of this code knows the
names of individual queries. It only knows the name of the query group storage
struct. This means that we often delegate things to the group -- e.g., the
database key is composed of group keys. This is similar to how none of the code
in the query group knows the full set of query groups, and so it must use
associated types from the Database
trait whenever it needs to put something in
a "global" context.
The database storage struct
The __SalsaDatabaseStorage
struct concatenates all of the query group storage
structs. In the hello world example, it looks something like:
struct __SalsaDatabaseStorage {
hello_world: <HelloWorldStorage as salsa::plumbing::QueryGroup<DatabaseStruct>>::GroupStorage
}
We also generate a Default
impl for __SalsaDatabaseStorage
. It invokes
a new
method on each group storage with the unique index assigned to that group.
This invokes the inherent new
method generated by the #[salsa::query_group]
macro.
The HasQueryGroup
impl
The HasQueryGroup
trait allows a given query group to access its definition
within the greater database. The impl is generated here:
has_group_impls.extend(quote! {
impl salsa::plumbing::HasQueryGroup<#group_path> for #database_name {
fn group_storage(&self) -> &#group_storage {
&self.#db_storage_field.query_store().#group_name_snake
}
fn group_storage_mut(&mut self) -> (&#group_storage, &mut salsa::Runtime) {
let (query_store_mut, runtime) = self.#db_storage_field.query_store_mut();
(&query_store_mut.#group_name_snake, runtime)
}
}
});
The HasQueryGroup
impl combines with the blanket impl from the
#[salsa::query_group]
macro so that the database can implement the query group
trait (e.g., the HelloWorld
trait) but without knowing all the names of the
query methods and the like.
The DatabaseStorageTypes
impl
Then there are a variety of other impls, like this one for DatabaseStorageTypes
:
output.extend(quote! {
impl salsa::plumbing::DatabaseStorageTypes for #database_name {
type DatabaseStorage = __SalsaDatabaseStorage;
}
});
The DatabaseOps
impl
Or this one for DatabaseOps
, which defines the for-each method to
invoke an operation on every kind of query in the database. It ultimately
delegates to the for_each
methods for the groups:
let mut fmt_ops = proc_macro2::TokenStream::new();
let mut maybe_changed_ops = proc_macro2::TokenStream::new();
let mut cycle_recovery_strategy_ops = proc_macro2::TokenStream::new();
let mut for_each_ops = proc_macro2::TokenStream::new();
for ((QueryGroup { group_path }, group_storage), group_index) in query_groups
.iter()
.zip(&query_group_storage_names)
.zip(0_u16..)
{
fmt_ops.extend(quote! {
group_index => {
let storage: &#group_storage =
<Self as salsa::plumbing::HasQueryGroup<#group_path>>::group_storage(self);
storage.fmt_index(self, input, fmt)
}
});
maybe_changed_ops.extend(quote! {
group_index => {
let storage: &#group_storage =
<Self as salsa::plumbing::HasQueryGroup<#group_path>>::group_storage(self);
storage.maybe_changed_after(self, input, revision)
}
});
cycle_recovery_strategy_ops.extend(quote! {
group_index => {
let storage: &#group_storage =
<Self as salsa::plumbing::HasQueryGroup<#group_path>>::group_storage(self);
storage.cycle_recovery_strategy(self, input)
}
});
for_each_ops.extend(quote! {
let storage: &#group_storage =
<Self as salsa::plumbing::HasQueryGroup<#group_path>>::group_storage(self);
storage.for_each_query(runtime, &mut op);
});
}
output.extend(quote! {
impl salsa::plumbing::DatabaseOps for #database_name {
fn ops_database(&self) -> &dyn salsa::Database {
self
}
fn ops_salsa_runtime(&self) -> &salsa::Runtime {
self.#db_storage_field.salsa_runtime()
}
fn ops_salsa_runtime_mut(&mut self) -> &mut salsa::Runtime {
self.#db_storage_field.salsa_runtime_mut()
}
fn fmt_index(
&self,
input: salsa::DatabaseKeyIndex,
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match input.group_index() {
fmt_ops
i => panic!("salsa: invalid group index {}", i)
}
}
fn maybe_changed_after(
&self,
input: salsa::DatabaseKeyIndex,
revision: salsa::Revision
) -> bool {
match input.group_index() {
maybe_changed_ops
i => panic!("salsa: invalid group index {}", i)
}
}
fn cycle_recovery_strategy(
&self,
input: salsa::DatabaseKeyIndex,
) -> salsa::plumbing::CycleRecoveryStrategy {
match input.group_index() {
cycle_recovery_strategy_ops
i => panic!("salsa: invalid group index {}", i)
}
}
fn for_each_query(
&self,
mut op: &mut dyn FnMut(&dyn salsa::plumbing::QueryStorageMassOps),
) {
let runtime = salsa::Database::salsa_runtime(self);
for_each_ops
}
}
});