use std::borrow::Cow;
use anyhow::Result;
use tracing::{debug, warn};
use crate::catalog::providers::{DatabaseProvider, NamespaceProvider};
use crate::catalog::{DatabaseId, NamespaceId, StoredModuleDefinition};
use crate::key::schema::{ModuleKey, ModulePrefix};
use crate::key::{KVKey, KVValue, Key};
use crate::kvs::TransactionType;
use crate::kvs::ds::Datastore;
const TARGET: &str = "surrealdb::core::kvs::migration";
pub(super) async fn rekey_definitions_to_their_derived_name(ds: &Datastore) -> Result<()> {
let txn = ds.transaction(TransactionType::Read).await?;
let databases = async {
let namespaces = txn.all_ns(None).await?;
let mut databases = Vec::new();
for ns in namespaces.iter() {
for db in txn.all_db(ns.namespace_id, None).await?.iter() {
databases.push((ns.namespace_id, db.database_id));
}
}
Ok::<_, anyhow::Error>(databases)
}
.await;
txn.cancel().await?;
let databases = databases?;
let mut moved = 0usize;
for (ns, db) in databases {
moved += migrate_database(ds, ns, db).await?;
}
if moved > 0 {
debug!(target: TARGET, count = moved, "Re-keyed module definitions to their derived name");
}
Ok(())
}
async fn migrate_database(ds: &Datastore, ns: NamespaceId, db: DatabaseId) -> Result<usize> {
let drifted = collect_drifted(ds, ns, db).await?;
for row in &drifted {
rekey(ds, ns, db, row).await?;
}
Ok(drifted.len())
}
struct Drifted {
stored_at: Key<'static>,
derived: String,
definition: StoredModuleDefinition,
}
async fn collect_drifted(ds: &Datastore, ns: NamespaceId, db: DatabaseId) -> Result<Vec<Drifted>> {
let txn = ds.transaction(TransactionType::Read).await?;
let range = catch!(
txn,
ModulePrefix {
ns,
db,
}
.range()
);
let rows = catch!(txn, txn.getr_raw(range, None).await);
txn.cancel().await?;
let mut drifted = Vec::new();
for (key, value) in rows {
let definition = match StoredModuleDefinition::kv_decode_value(&value, ()) {
Ok(definition) => definition,
Err(e) => {
warn!(
target: TARGET,
error = %e,
"Skipping a key in the module band that does not decode as a \
module definition"
);
continue;
}
};
let derived = match definition.get_storage_name() {
Ok(derived) => derived,
Err(e) => {
warn!(
target: TARGET,
error = %e,
"Skipping a module definition whose value derives no storage name"
);
continue;
}
};
let belongs_at = ModuleKey::new(ns, db, Cow::Borrowed(derived.as_str())).encode_key()?;
if belongs_at.as_ref() == key.as_slice() {
continue;
}
drifted.push(Drifted {
stored_at: Key::from(key),
derived,
definition,
});
}
Ok(drifted)
}
async fn rekey(ds: &Datastore, ns: NamespaceId, db: DatabaseId, row: &Drifted) -> Result<()> {
let key = ModuleKey::new(ns, db, Cow::Borrowed(row.derived.as_str()));
let txn = ds.transaction(TransactionType::Write).await?;
let created = txn.put_key(&key, &row.definition).await;
match run!(txn, created) {
Ok(()) => {}
Err(e) if super::already_exists(&e) => {
warn!(
target: TARGET,
name = %row.derived,
"A module definition already exists at the derived name; dropping the \
unreachable copy stored under the older one"
);
}
Err(e) => return Err(e),
}
let txn = ds.transaction(TransactionType::Write).await?;
let deleted = txn.del(row.stored_at.clone()).await;
run!(txn, deleted)
}
#[cfg(test)]
mod tests {
use super::*;
const LEGACY_SILO: &str = "silo::acme::widgets<1.2.3>";
const DERIVED_SILO: &str = "silo::acme::widgets::<1.2.3>";
#[test]
fn the_legacy_layout_matches_what_3_2_wrote() {
let key = ModuleKey::new(NamespaceId(1), DatabaseId(2), Cow::Borrowed(LEGACY_SILO))
.encode_key()
.unwrap();
assert_eq!(
key.as_ref(),
b"/*\x00\x00\x00\x01*\x00\x00\x00\x02!mdsilo::acme::widgets<1.2.3>\0"
);
}
#[test]
fn the_derived_silo_name_is_not_the_one_3_2_wrote() {
assert_eq!(silo(1, 2, 3).get_storage_name().unwrap(), DERIVED_SILO);
assert_ne!(DERIVED_SILO, LEGACY_SILO);
}
#[test]
fn a_named_module_derives_the_name_it_always_has() {
assert_eq!(surrealism("plain").get_storage_name().unwrap(), "mod::plain");
}
fn silo(major: u32, minor: u32, patch: u32) -> StoredModuleDefinition {
use crate::catalog::{ModuleExecutable, SiloExecutable, StoredPermission};
StoredModuleDefinition {
name: None,
comment: None,
permissions: StoredPermission::Full,
executable: ModuleExecutable::Silo(SiloExecutable {
organisation: "acme".to_string(),
package: "widgets".to_string(),
major,
minor,
patch,
}),
unsigned: true,
}
}
fn surrealism(name: &str) -> StoredModuleDefinition {
use crate::catalog::{ModuleExecutable, StoredPermission, SurrealismExecutable};
StoredModuleDefinition {
name: Some(name.to_string()),
comment: None,
permissions: StoredPermission::Full,
executable: ModuleExecutable::Surrealism(SurrealismExecutable {
bucket: "mods".to_string(),
key: format!("/{name}.wasm"),
}),
unsigned: true,
}
}
#[cfg(feature = "kv-mem")]
mod against_a_datastore {
use std::sync::Arc;
use super::*;
use crate::catalog::{DatabaseDefinition, NamespaceDefinition};
use crate::kvs::Datastore;
async fn fixture() -> (Arc<Datastore>, NamespaceId, DatabaseId) {
let ds = Datastore::new("memory").await.unwrap();
let (ns, db) = (NamespaceId(1), DatabaseId(1));
let txn = ds.transaction(TransactionType::Write).await.unwrap();
txn.put_ns(NamespaceDefinition {
namespace_id: ns,
name: "test".into(),
comment: None,
})
.await
.unwrap();
txn.put_db(
"test",
DatabaseDefinition {
namespace_id: ns,
database_id: db,
name: "test".into(),
strict: false,
comment: None,
changefeed: None,
},
)
.await
.unwrap();
txn.commit().await.unwrap();
(ds, ns, db)
}
async fn write_at(
ds: &Datastore,
ns: NamespaceId,
db: DatabaseId,
name: &str,
definition: &StoredModuleDefinition,
) {
let txn = ds.transaction(TransactionType::Write).await.unwrap();
txn.set_key(&ModuleKey::new(ns, db, Cow::Borrowed(name)), definition).await.unwrap();
txn.commit().await.unwrap();
}
async fn band(ds: &Datastore, ns: NamespaceId, db: DatabaseId) -> Vec<(Vec<u8>, Vec<u8>)> {
let range = ModulePrefix {
ns,
db,
}
.range()
.unwrap();
let txn = ds.transaction(TransactionType::Read).await.unwrap();
let found = txn.getr_raw(range, None).await.unwrap();
txn.cancel().await.unwrap();
found
}
async fn names(ds: &Datastore, ns: NamespaceId, db: DatabaseId) -> Vec<String> {
use crate::key::KVKeyDecode as _;
band(ds, ns, db)
.await
.into_iter()
.map(|(k, _)| ModuleKey::decode_key(&k).unwrap().md.into_owned())
.collect()
}
async fn addressable(ds: &Datastore, ns: NamespaceId, db: DatabaseId, name: &str) -> bool {
let txn = ds.transaction(TransactionType::Read).await.unwrap();
let found = txn.get_db_module(ns, db, name, None).await.is_ok();
txn.cancel().await.unwrap();
found
}
#[tokio::test]
async fn a_legacy_silo_definition_becomes_addressable() {
let (ds, ns, db) = fixture().await;
let definition = silo(1, 2, 3);
write_at(&ds, ns, db, LEGACY_SILO, &definition).await;
assert!(!addressable(&ds, ns, db, DERIVED_SILO).await);
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(names(&ds, ns, db).await, vec![DERIVED_SILO]);
let stored = &band(&ds, ns, db).await[0].1;
assert_eq!(StoredModuleDefinition::kv_decode_value(stored, ()).unwrap(), definition);
assert!(addressable(&ds, ns, db, DERIVED_SILO).await);
}
#[tokio::test]
async fn a_named_module_is_left_exactly_where_it_is() {
let (ds, ns, db) = fixture().await;
write_at(&ds, ns, db, "mod::plain", &surrealism("plain")).await;
let before = band(&ds, ns, db).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(band(&ds, ns, db).await, before);
}
#[tokio::test]
async fn a_definition_at_its_derived_key_is_untouched() {
let (ds, ns, db) = fixture().await;
write_at(&ds, ns, db, DERIVED_SILO, &silo(1, 2, 3)).await;
let before = band(&ds, ns, db).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(band(&ds, ns, db).await, before);
}
#[tokio::test]
async fn a_duplicate_resolves_in_favour_of_the_addressable_row() {
let (ds, ns, db) = fixture().await;
write_at(&ds, ns, db, LEGACY_SILO, &silo(1, 2, 3)).await;
let mut overwritten = silo(1, 2, 3);
overwritten.comment = Some("written to work around the break".to_string());
write_at(&ds, ns, db, DERIVED_SILO, &overwritten).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(names(&ds, ns, db).await, vec![DERIVED_SILO]);
let stored = &band(&ds, ns, db).await[0].1;
assert_eq!(StoredModuleDefinition::kv_decode_value(stored, ()).unwrap(), overwritten);
}
#[tokio::test]
async fn running_it_twice_converges() {
let (ds, ns, db) = fixture().await;
write_at(&ds, ns, db, LEGACY_SILO, &silo(1, 2, 3)).await;
write_at(&ds, ns, db, "mod::plain", &surrealism("plain")).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
let once = band(&ds, ns, db).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(band(&ds, ns, db).await, once);
assert_eq!(names(&ds, ns, db).await, vec!["mod::plain", DERIVED_SILO]);
}
#[tokio::test]
async fn each_database_is_migrated() {
let (ds, ns, db) = fixture().await;
let other = DatabaseId(2);
let txn = ds.transaction(TransactionType::Write).await.unwrap();
txn.put_db(
"other",
DatabaseDefinition {
namespace_id: ns,
database_id: other,
name: "other".into(),
strict: false,
comment: None,
changefeed: None,
},
)
.await
.unwrap();
txn.commit().await.unwrap();
write_at(&ds, ns, db, LEGACY_SILO, &silo(1, 2, 3)).await;
write_at(&ds, ns, other, LEGACY_SILO, &silo(1, 2, 3)).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(names(&ds, ns, db).await, vec![DERIVED_SILO]);
assert_eq!(names(&ds, ns, other).await, vec![DERIVED_SILO]);
}
#[tokio::test]
async fn an_undecodable_row_is_left_in_place() {
let (ds, ns, db) = fixture().await;
write_at(&ds, ns, db, LEGACY_SILO, &silo(1, 2, 3)).await;
let junk = ModuleKey::new(ns, db, Cow::Borrowed("mod::junk")).encode_key().unwrap();
let txn = ds.transaction(TransactionType::Write).await.unwrap();
txn.set(junk.clone(), b"not a module definition".to_vec()).await.unwrap();
txn.commit().await.unwrap();
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
let mut found = names(&ds, ns, db).await;
found.sort();
assert_eq!(found, vec!["mod::junk", DERIVED_SILO]);
}
#[tokio::test]
async fn the_shipped_registry_selects_and_runs_it() {
use crate::key::schema::MigrationKey;
use crate::kvs::migration::{MIGRATIONS, run_with};
use crate::kvs::version::StorageVersion;
let (ds, ns, db) = fixture().await;
write_at(&ds, ns, db, LEGACY_SILO, &silo(1, 2, 3)).await;
run_with(&ds, MIGRATIONS, &StorageVersion::current(), false).await.unwrap();
assert_eq!(names(&ds, ns, db).await, vec![DERIVED_SILO]);
let txn = ds.transaction(TransactionType::Read).await.unwrap();
let recorded = txn.exists_key(&MigrationKey::new(2), None).await.unwrap();
txn.cancel().await.unwrap();
assert!(recorded);
}
#[cfg(feature = "surrealism")]
#[tokio::test]
async fn remove_module_reaches_a_migrated_definition() {
use crate::catalog::providers::CatalogProvider as _;
use crate::dbs::Session;
use crate::dbs::capabilities::{Capabilities, Targets};
use crate::kvs::migration::{MIGRATIONS, run_with};
use crate::kvs::version::StorageVersion;
let ds = crate::kvs::Datastore::builder()
.with_capabilities(Capabilities::all().with_experimental(Targets::All))
.build_with_path("memory")
.await
.unwrap();
let txn = ds.transaction(TransactionType::Write).await.unwrap();
let database = txn.ensure_ns_db(None, "test", "test").await.unwrap();
txn.commit().await.unwrap();
let (ns, db) = (database.namespace_id, database.database_id);
write_at(&ds, ns, db, LEGACY_SILO, &silo(1, 2, 3)).await;
let session = Session::owner().with_ns("test").with_db("test");
let remove = format!("REMOVE MODULE {DERIVED_SILO}");
let before = ds.execute(&remove, &session, None).await.unwrap();
let Err(e) = &before[0].result else {
panic!(
"a definition under the older derivation was reachable before the migration"
);
};
assert!(e.to_string().contains(DERIVED_SILO), "unexpected error: {e}");
run_with(&ds, MIGRATIONS, &StorageVersion::current(), false).await.unwrap();
let after = ds.execute(&remove, &session, None).await.unwrap();
after[0].result.as_ref().expect("the migrated definition is addressable by name");
assert!(names(&ds, ns, db).await.is_empty());
}
#[tokio::test]
async fn a_definition_with_no_derivable_name_is_left_in_place() {
use crate::catalog::{ModuleExecutable, StoredPermission, SurrealismExecutable};
let (ds, ns, db) = fixture().await;
let nameless = StoredModuleDefinition {
name: None,
comment: None,
permissions: StoredPermission::None,
executable: ModuleExecutable::Surrealism(SurrealismExecutable {
bucket: "mods".to_string(),
key: "/anonymous.wasm".to_string(),
}),
unsigned: true,
};
assert!(nameless.get_storage_name().is_err());
write_at(&ds, ns, db, "mod::anonymous", &nameless).await;
let before = band(&ds, ns, db).await;
rekey_definitions_to_their_derived_name(&ds).await.unwrap();
assert_eq!(band(&ds, ns, db).await, before);
}
}
}