use futures::{FutureExt, future::BoxFuture};
use slatedb::DbTransaction;
use crate::{
error::{Error, Result},
store::{
handle::ReadHandle,
key::{EntityKey, Key},
proto,
read::{EntityRecord, scan_current_entities},
value,
},
transaction::{
commit::{FORMAT_VERSION, MAX_FORMAT_VERSION},
migration::{MigrationUnit, StepOutcome},
},
};
pub(crate) const SOURCE_SCOPE: u64 = 1;
pub(crate) const TARGET_SCOPE: u64 = 2;
fn move_scope_step<'a>(
tx: &'a DbTransaction,
cursor: &'a [u8],
) -> BoxFuture<'a, Result<StepOutcome>> {
async move {
let start = decode_cursor(cursor)?;
let Some((scope_id, value)) = next_source_record(tx, start).await? else {
return Ok(None);
};
tx.put(
Key::current(EntityKey::Option {
scope_kind: TARGET_SCOPE,
scope_id,
})
.encode(),
value::encode_value(&value),
)
.map_err(Error::from)?;
tx.delete(
Key::current(EntityKey::Option {
scope_kind: SOURCE_SCOPE,
scope_id,
})
.encode(),
)
.map_err(Error::from)?;
Ok(Some(scope_id.to_be_bytes().to_vec()))
}
.boxed()
}
fn decode_cursor(cursor: &[u8]) -> Result<Option<u64>> {
if cursor.is_empty() {
return Ok(None);
}
let bytes: [u8; 8] = cursor
.try_into()
.map_err(|_| Error::Corruption("migration cursor is not a scope id".to_string()))?;
Ok(Some(u64::from_be_bytes(bytes)))
}
async fn next_source_record(
tx: &DbTransaction,
start: Option<u64>,
) -> Result<Option<(u64, proto::OptionScopeValue)>> {
let mut found: Vec<(u64, proto::OptionScopeValue)> = scan_current_entities(ReadHandle::Tx(tx))
.await?
.into_iter()
.filter_map(|record| match record {
EntityRecord::Option {
scope_kind,
scope_id,
value,
} if scope_kind == SOURCE_SCOPE => Some((scope_id, value)),
_ => None,
})
.filter(|(scope_id, _)| start.is_none_or(|start| *scope_id > start))
.collect();
found.sort_by_key(|(scope_id, _)| *scope_id);
Ok(found.into_iter().next())
}
const MOVE_SCOPE: MigrationUnit = MigrationUnit {
name: "move-option-scope",
from_format: FORMAT_VERSION,
to_format: MAX_FORMAT_VERSION,
step: move_scope_step,
};
fn no_work_step<'a>(
_tx: &'a DbTransaction,
_cursor: &'a [u8],
) -> BoxFuture<'a, Result<StepOutcome>> {
async move { Ok(None) }.boxed()
}
const SECOND_LINK: MigrationUnit = MigrationUnit {
name: "second-link",
from_format: MAX_FORMAT_VERSION,
to_format: MAX_FORMAT_VERSION + 1,
step: no_work_step,
};
pub(crate) const REWRITE: &[&MigrationUnit] = &[&MOVE_SCOPE];
pub(crate) const REWRITE_THEN_LINK: &[&MigrationUnit] = &[&MOVE_SCOPE, &SECOND_LINK];