use std::ops::Range;
use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use crate::err::Error;
use crate::kvs::version::MajorVersion;
use crate::kvs::{Datastore, Error as KvsError, Key, LockType, Transaction, TransactionType};
pub const VERSION_KEY: &[u8] = b"!v";
pub const BOOTSTRAP_KEY: &[u8] = b"!bv";
const LEDGER_PREFIX: &[u8] = b"/!mg";
const LEDGER_END: &[u8] = b"/!mh";
const KEYSPACE_START: u8 = 0x00;
const KEYSPACE_END: u8 = 0xff;
pub fn keyspace() -> Range<Key> {
vec![KEYSPACE_START]..vec![KEYSPACE_END]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyRole {
Data,
Version,
Skipped,
}
pub fn key_role(key: &[u8]) -> KeyRole {
if key == VERSION_KEY {
KeyRole::Version
} else if key == BOOTSTRAP_KEY {
KeyRole::Skipped
} else {
KeyRole::Data
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyspaceVersion {
pub major: Option<u16>,
pub stamp: Option<String>,
pub migrations: Vec<u32>,
}
impl KeyspaceVersion {
pub async fn read(tx: &Transaction) -> Result<Self> {
let major = tx.get(&crate::key::version::new(), None).await?.map(u16::from);
let keys = tx.keys(LEDGER_PREFIX.to_vec()..LEDGER_END.to_vec(), u32::MAX, 0, None).await?;
let migrations = keys.iter().map(|key| ledger_id(key)).collect::<Result<_>>()?;
Ok(Self {
major,
stamp: None,
migrations,
})
}
pub fn check(&self) -> Result<()> {
let major = self.major.map(MajorVersion::from).unwrap_or_else(MajorVersion::v1);
if !major.is_latest() {
bail!(Error::OutdatedStorageVersion {
expected: MajorVersion::latest().into(),
actual: major.into(),
});
}
Ok(())
}
}
fn ledger_id(key: &[u8]) -> Result<u32> {
let Some(id) = key.strip_prefix(LEDGER_PREFIX).and_then(|id| <[u8; 4]>::try_from(id).ok())
else {
bail!("a key in the migration ledger's range is not a ledger entry");
};
Ok(u32::from_be_bytes(id))
}
pub async fn keeps_history(datastore: &Datastore) -> Result<bool> {
let tx = datastore.transaction(TransactionType::Read, LockType::Optimistic).await?;
let probe = tx.get(&crate::key::version::new(), Some(1)).await;
tx.cancel().await?;
match probe {
Ok(_) => Ok(true),
Err(err) if refuses_versioned_reads(&err) => Ok(false),
Err(err) => Err(err),
}
}
fn refuses_versioned_reads(err: &anyhow::Error) -> bool {
matches!(
err.downcast_ref::<Error>(),
Some(
Error::Kvs(KvsError::UnsupportedVersionedQueries) | Error::UnsupportedVersionedQueries
)
) || matches!(err.downcast_ref::<KvsError>(), Some(KvsError::UnsupportedVersionedQueries))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dbs::Session;
use crate::kvs::KVKey;
async fn seeded(path: &str) -> Result<Datastore> {
let ds = Datastore::new(path).await?;
ds.check_version().await?;
let session = Session::owner().with_ns("t").with_db("t");
let sql = "
DEFINE NAMESPACE t; DEFINE DATABASE t;
DEFINE USER root_reader ON ROOT PASSWORD 'x' ROLES VIEWER;
DEFINE TABLE person SCHEMAFULL CHANGEFEED 1d;
DEFINE FIELD name ON person TYPE string;
DEFINE INDEX person_name ON person FIELDS name UNIQUE;
DEFINE ANALYZER simple TOKENIZERS blank FILTERS lowercase;
DEFINE INDEX person_search ON person FIELDS name FULLTEXT ANALYZER simple BM25;
DEFINE TABLE knows TYPE RELATION;
DEFINE EVENT noted ON person WHEN true THEN (CREATE note SET at = time::now());
DEFINE FUNCTION fn::one() { RETURN 1 };
DEFINE PARAM $answer VALUE 42;
DEFINE SEQUENCE ids;
CREATE person:1 SET name = 'one';
CREATE person:2 SET name = 'two';
RELATE person:1->knows->person:2;
";
for result in ds.execute(sql, &session, None).await? {
result.result.map_err(|err| anyhow::anyhow!("seeding failed: {err}"))?;
}
Ok(ds)
}
#[test]
fn the_named_keys_are_the_ones_the_engine_encodes() -> Result<()> {
assert_eq!(crate::key::version::new().encode_key()?, VERSION_KEY);
assert_eq!(key_role(VERSION_KEY), KeyRole::Version);
assert_eq!(key_role(BOOTSTRAP_KEY), KeyRole::Skipped);
assert_eq!(key_role(b"/!vs"), KeyRole::Data);
assert_eq!(key_role(b"/!mg\0\0\0\x01"), KeyRole::Data);
assert_eq!(key_role(b"!vv"), KeyRole::Data);
assert_eq!(keyspace(), vec![0x00]..vec![0xff]);
Ok(())
}
#[tokio::test]
async fn every_key_the_engine_writes_is_inside_the_copied_range() -> Result<()> {
let ds = seeded("memory").await?;
let tx = ds.transaction(TransactionType::Read, LockType::Optimistic).await?;
let keyspace = tx.keys(keyspace(), u32::MAX, 0, None).await;
let above = tx.keys(vec![KEYSPACE_END]..vec![KEYSPACE_END; 64], u32::MAX, 0, None).await;
tx.cancel().await?;
let keyspace = keyspace?;
assert!(keyspace.len() > 40, "the seed did not write a keyspace worth checking");
let outside: Vec<&Key> = keyspace.iter().filter(|key| !key.starts_with(b"/")).collect();
assert_eq!(outside, [&VERSION_KEY.to_vec()]);
assert!(keyspace.iter().all(|key| key_role(key) != KeyRole::Skipped));
assert_eq!(above?, Vec::<Key>::new());
Ok(())
}
fn version(major: Option<u16>, migrations: &[u32]) -> KeyspaceVersion {
KeyspaceVersion {
major,
stamp: Some("9.9.9".to_owned()),
migrations: migrations.to_vec(),
}
}
#[test]
fn a_keyspace_this_build_can_start_from_passes() -> Result<()> {
version(Some(MajorVersion::LATEST), &[1, 2, 3]).check()?;
version(Some(MajorVersion::LATEST), &[]).check()?;
Ok(())
}
#[test]
fn an_older_or_unversioned_major_is_refused_as_out_of_date() {
for major in [None, Some(1), Some(2)] {
let err = version(major, &[]).check().unwrap_err();
assert!(
matches!(err.downcast_ref::<Error>(), Some(Error::OutdatedStorageVersion { .. })),
"{major:?}: {err}"
);
}
}
#[tokio::test]
async fn a_running_store_describes_itself_as_startable() -> Result<()> {
let ds = seeded("memory").await?;
let tx = ds.transaction(TransactionType::Read, LockType::Optimistic).await?;
let described = KeyspaceVersion::read(&tx).await;
tx.cancel().await?;
let described = described?;
assert_eq!(
described,
KeyspaceVersion {
major: Some(MajorVersion::LATEST),
stamp: None,
migrations: Vec::new(),
}
);
described.check()?;
Ok(())
}
#[test]
fn the_description_serialises_with_a_fixed_shape() -> Result<()> {
let described = KeyspaceVersion {
major: Some(3),
stamp: None,
migrations: vec![1, 7],
};
assert_eq!(
serde_json::to_string(&described)?,
r#"{"major":3,"stamp":null,"migrations":[1,7]}"#
);
assert_eq!(
serde_json::from_str::<KeyspaceVersion>(&serde_json::to_string(&described)?)?,
described
);
assert_eq!(
serde_json::to_string(&KeyspaceVersion::default())?,
r#"{"major":null,"stamp":null,"migrations":[]}"#
);
Ok(())
}
async fn described(ds: &Datastore) -> Result<KeyspaceVersion> {
let tx = ds.transaction(TransactionType::Read, LockType::Optimistic).await?;
let described = KeyspaceVersion::read(&tx).await;
tx.cancel().await?;
described
}
#[tokio::test]
async fn the_ledger_a_later_release_wrote_is_named() -> Result<()> {
let ds = seeded("memory").await?;
let tx = ds.transaction(TransactionType::Write, LockType::Optimistic).await?;
for id in [300u32, 2_000_000_000, u32::MAX, 1] {
let mut key = LEDGER_PREFIX.to_vec();
key.extend_from_slice(&id.to_be_bytes());
tx.set(&key, &b"record".to_vec()).await?;
}
for key in [&b"/!md"[..], b"/!mf\xff\xff\xff\xff", b"/!mh\0\0\0\x01", b"/!vs"] {
tx.set(&key.to_vec(), &b"x".to_vec()).await?;
}
tx.commit().await?;
assert_eq!(described(&ds).await?.migrations, [1, 300, 2_000_000_000, u32::MAX]);
Ok(())
}
#[tokio::test]
async fn a_malformed_ledger_key_is_not_described_as_a_ledger() -> Result<()> {
let ds = seeded("memory").await?;
let tx = ds.transaction(TransactionType::Write, LockType::Optimistic).await?;
tx.set(&b"/!mg\0\0\x01".to_vec(), &b"x".to_vec()).await?;
tx.commit().await?;
assert!(described(&ds).await.is_err());
Ok(())
}
#[tokio::test]
async fn a_memory_store_keeps_no_history() -> Result<()> {
let ds = Datastore::new("memory").await?;
assert!(!keeps_history(&ds).await?);
Ok(())
}
#[tokio::test]
async fn a_store_keeps_history_only_when_it_is_versioned() -> Result<()> {
let plain = Datastore::new("memory").await?;
assert!(!keeps_history(&plain).await?);
let versioned = Datastore::new("memory?versioned=true").await?;
assert!(keeps_history(&versioned).await?);
versioned.check_version().await?;
assert!(keeps_history(&versioned).await?);
Ok(())
}
}