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 the HelloWorldStorage struct itself.
  • an impl of HasQueryGroup<G> for each query group G
  • 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
            }
        }
    });