use super::*;
use crate::metadata::{
Artifact, ArtifactState, ArtifactType, Context, ContextType, Execution, ExecutionState,
ExecutionType, PropertyValue,
};
use tempfile::NamedTempFile;
#[tokio::test(flavor = "multi_thread")]
async fn initialization_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
MetadataStore::connect(&sqlite_uri(file.path())).await?;
let file = existing_db();
MetadataStore::connect(&sqlite_uri(file.path())).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_artifact_type_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
store
.put_artifact_type("t0")
.property("p0", PropertyType::Int)
.execute()
.await?;
assert!(matches!(
store
.put_artifact_type("t0")
.property("p0", PropertyType::Double)
.execute()
.await,
Err(PutError::TypeAlreadyExists { .. })
));
assert!(matches!(
store
.put_artifact_type("t0")
.property("p0", PropertyType::Int)
.property("p1", PropertyType::String)
.execute()
.await,
Err(PutError::TypeAlreadyExists { .. })
));
store
.put_artifact_type("t0")
.can_add_fields()
.property("p0", PropertyType::Int)
.property("p1", PropertyType::String)
.execute()
.await?;
assert!(matches!(
store.put_artifact_type("t0").execute().await,
Err(PutError::TypeAlreadyExists { .. })
));
store
.put_artifact_type("t0")
.can_omit_fields()
.execute()
.await?;
store.put_artifact_type("t1").execute().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_artifact_type_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let t0_id = store
.put_artifact_type("t0")
.property("p0", PropertyType::Int)
.execute()
.await?;
let t1_id = store.put_artifact_type("t1").execute().await?;
assert_ne!(t0_id, t1_id);
assert_eq!(
store.get_artifact_types().name("t0").execute().await?[0],
ArtifactType {
id: t0_id,
name: "t0".to_owned(),
properties: vec![("p0".to_owned(), PropertyType::Int)]
.into_iter()
.collect()
}
);
assert_eq!(
store.get_artifact_types().name("t1").execute().await?[0],
ArtifactType {
id: t1_id,
name: "t1".to_owned(),
properties: BTreeMap::new(),
}
);
assert!(store
.get_artifact_types()
.name("t2")
.execute()
.await?
.is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_artifact_types_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let types = store.get_artifact_types().execute().await?;
assert_eq!(types.len(), 6);
assert_eq!(types[0].name, "mlmd.Dataset");
assert_eq!(types[1].name, "mlmd.Model");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_artifacts_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let mut artifact0 = artifact0();
artifact0.type_id = TypeId::new(10);
let artifacts = store.get_artifacts().execute().await?;
assert_eq!(artifacts, vec![artifact0.clone(), artifact1()]);
let artifacts = store.get_artifacts().ty("DataSet").execute().await?;
assert_eq!(artifacts, vec![artifact0]);
let unregistered_id = ArtifactId::new(100);
let artifacts = store
.get_artifacts()
.ids([ArtifactId::new(2), unregistered_id].iter().copied())
.execute()
.await?;
assert_eq!(artifacts[0].id.get(), 2);
assert_eq!(artifacts, vec![artifact1()]);
let artifacts = store
.get_artifacts()
.uri("path/to/model/file")
.execute()
.await?;
assert_eq!(artifacts, vec![artifact1()]);
let artifacts = store
.get_artifacts()
.context(ContextId::new(1))
.execute()
.await?;
assert_eq!(artifacts, vec![artifact1()]);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn post_artifact_works() -> anyhow::Result<()> {
let file = NamedTempFile::new().unwrap();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
assert!(store.get_artifacts().execute().await?.is_empty());
let type_id = store
.put_artifact_type("DataSet")
.property("day", PropertyType::Int)
.property("split", PropertyType::String)
.execute()
.await?;
let artifact_id = store.post_artifact(type_id).execute().await?;
let artifacts = store.get_artifacts().execute().await?;
assert_eq!(artifacts.len(), 1);
assert_eq!(artifacts[0].id, artifact_id);
let mut expected = artifact0();
expected.id = store
.post_artifact(type_id)
.uri(expected.uri.as_ref().unwrap())
.properties(expected.properties.clone())
.execute()
.await?;
let artifacts = store.get_artifacts().execute().await?;
assert_eq!(artifacts.len(), 2);
expected.create_time_since_epoch = artifacts[1].create_time_since_epoch;
expected.last_update_time_since_epoch = artifacts[1].last_update_time_since_epoch;
assert_eq!(artifacts[1], expected);
store.post_artifact(type_id).name("foo").execute().await?;
assert!(matches!(
store
.post_artifact(type_id)
.name("foo")
.execute()
.await
.err(),
Some(PostError::NameAlreadyExists { .. })
));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_artifact_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
assert_eq!(store.get_artifacts().execute().await?.len(), 2);
let mut artifact = artifact0();
artifact.type_id = TypeId::new(10);
artifact.name = Some("foo".to_string());
artifact.state = ArtifactState::Live;
artifact
.properties
.insert("day".to_owned(), PropertyValue::Int(234));
artifact
.custom_properties
.insert("bar".to_string(), PropertyValue::Int(10));
store
.put_artifact(artifact.id)
.name(artifact.name.as_ref().unwrap().as_str())
.state(artifact.state)
.properties(artifact.properties.clone())
.custom_properties(artifact.custom_properties.clone())
.execute()
.await?;
let artifacts = store.get_artifacts().id(artifact.id).execute().await?;
assert_eq!(artifacts.len(), 1);
artifact.last_update_time_since_epoch = artifacts[0].last_update_time_since_epoch;
assert_eq!(artifacts[0], artifact);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_executions_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let executions = store.get_executions().execute().await?;
assert_eq!(executions, vec![execution0()]);
let executions = store.get_executions().ty("Trainer").execute().await?;
assert_eq!(executions, vec![execution0()]);
let executions = store.get_executions().ty("foo").execute().await?;
assert_eq!(executions, vec![]);
let unregistered_id = ExecutionId::new(100);
let executions = store
.get_executions()
.ids([ExecutionId::new(1), unregistered_id].iter().copied())
.execute()
.await?;
assert_eq!(executions, vec![execution0()]);
let executions = store
.get_executions()
.context(ContextId::new(1))
.execute()
.await?;
assert_eq!(executions, vec![execution0()]);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_execution_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
assert_eq!(store.get_executions().execute().await?.len(), 1);
let mut execution = execution0();
execution.name = Some("foo".to_string());
execution.last_known_state = ExecutionState::Running;
execution
.custom_properties
.insert("bar".to_string(), PropertyValue::Int(10));
store
.put_execution(execution.id)
.name(execution.name.as_ref().unwrap())
.state(execution.last_known_state)
.custom_properties(execution.custom_properties.clone())
.execute()
.await?;
let executions = store.get_executions().execute().await?;
assert_eq!(executions.len(), 1);
execution.last_update_time_since_epoch = executions[0].last_update_time_since_epoch;
assert_eq!(executions[0], execution);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn post_execution_works() -> anyhow::Result<()> {
let file = NamedTempFile::new().unwrap();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
assert!(store.get_executions().execute().await?.is_empty());
let type_id = store
.put_execution_type("DataSet")
.property("day", PropertyType::Int)
.property("split", PropertyType::String)
.execute()
.await?;
let execution_id = store.post_execution(type_id).execute().await?;
let executions = store.get_executions().execute().await?;
assert_eq!(executions.len(), 1);
assert_eq!(executions[0].id, execution_id);
store.post_execution(type_id).name("foo").execute().await?;
assert!(matches!(
store
.post_execution(type_id)
.name("foo")
.execute()
.await
.err(),
Some(PostError::NameAlreadyExists { .. })
));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_execution_type_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
store
.put_execution_type("t0")
.property("p0", PropertyType::Int)
.execute()
.await?;
assert!(matches!(
store
.put_execution_type("t0")
.property("p0", PropertyType::Double)
.execute()
.await,
Err(PutError::TypeAlreadyExists { .. })
));
assert!(matches!(
store
.put_execution_type("t0")
.property("p0", PropertyType::Int)
.property("p1", PropertyType::String)
.execute()
.await,
Err(PutError::TypeAlreadyExists { .. })
));
store
.put_execution_type("t0")
.can_add_fields()
.property("p0", PropertyType::Int)
.property("p1", PropertyType::String)
.execute()
.await?;
assert!(matches!(
store.put_execution_type("t0").execute().await,
Err(PutError::TypeAlreadyExists { .. })
));
store
.put_execution_type("t0")
.can_omit_fields()
.execute()
.await?;
store.put_execution_type("t1").execute().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_execution_type_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let t0_id = store
.put_execution_type("t0")
.property("p0", PropertyType::Int)
.execute()
.await?;
let t1_id = store.put_execution_type("t1").execute().await?;
assert_ne!(t0_id, t1_id);
assert_eq!(
store.get_execution_types().name("t0").execute().await?[0],
ExecutionType {
id: t0_id,
name: "t0".to_owned(),
properties: vec![("p0".to_owned(), PropertyType::Int)]
.into_iter()
.collect()
}
);
assert_eq!(
store.get_execution_types().name("t1").execute().await?[0],
ExecutionType {
id: t1_id,
name: "t1".to_owned(),
properties: BTreeMap::new(),
}
);
assert!(store
.get_execution_types()
.name("t2")
.execute()
.await?
.is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_execution_types_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let types = store.get_execution_types().execute().await?;
assert_eq!(types.len(), 6);
assert_eq!(types[0].name, "mlmd.Train");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_contexts_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let contexts = store.get_contexts().execute().await?;
assert_eq!(contexts, vec![context0()]);
let contexts = store.get_contexts().ty("Experiment").execute().await?;
assert_eq!(contexts, vec![context0()]);
let contexts = store.get_contexts().ty("foo").execute().await?;
assert_eq!(contexts, vec![]);
let contexts = store
.get_contexts()
.type_and_name("Experiment", "exp1")
.execute()
.await?;
assert_eq!(contexts, vec![context0()]);
let unregistered_id = ContextId::new(100);
let contexts = store
.get_contexts()
.ids([ContextId::new(1), unregistered_id].iter().copied())
.execute()
.await?;
assert_eq!(contexts, vec![context0()]);
let contexts = store
.get_contexts()
.artifact(ArtifactId::new(2))
.execute()
.await?;
assert_eq!(contexts, vec![context0()]);
let contexts = store
.get_contexts()
.execution(ExecutionId::new(1))
.execute()
.await?;
assert_eq!(contexts, vec![context0()]);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_context_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
assert_eq!(store.get_contexts().execute().await?.len(), 1);
let mut context = context0();
context.name = "foo".to_string();
context
.custom_properties
.insert("bar".to_string(), PropertyValue::Int(10));
store
.put_context(context.id)
.name(&context.name)
.custom_properties(context.custom_properties.clone())
.execute()
.await?;
let contexts = store.get_contexts().execute().await?;
assert_eq!(contexts.len(), 1);
context.last_update_time_since_epoch = contexts[0].last_update_time_since_epoch;
assert_eq!(contexts[0], context);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn post_context_works() -> anyhow::Result<()> {
let file = NamedTempFile::new().unwrap();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
assert!(store.get_contexts().execute().await?.is_empty());
let type_id = store.put_context_type("Context").execute().await?;
let context_id = store.post_context(type_id, "bar").execute().await?;
let contexts = store.get_contexts().execute().await?;
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].id, context_id);
store.post_context(type_id, "foo").execute().await?;
assert!(matches!(
store.post_context(type_id, "foo").execute().await.err(),
Some(PostError::NameAlreadyExists { .. })
));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_context_type_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
store
.put_context_type("t0")
.property("p0", PropertyType::Int)
.execute()
.await?;
assert!(matches!(
store
.put_context_type("t0")
.property("p0", PropertyType::Double)
.execute()
.await,
Err(PutError::TypeAlreadyExists { .. })
));
assert!(matches!(
store
.put_context_type("t0")
.property("p0", PropertyType::Int)
.property("p1", PropertyType::String)
.execute()
.await,
Err(PutError::TypeAlreadyExists { .. })
));
store
.put_context_type("t0")
.can_add_fields()
.property("p0", PropertyType::Int)
.property("p1", PropertyType::String)
.execute()
.await?;
assert!(matches!(
store.put_context_type("t0").execute().await,
Err(PutError::TypeAlreadyExists { .. })
));
store
.put_context_type("t0")
.can_omit_fields()
.execute()
.await?;
store.put_context_type("t1").execute().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_context_type_works() -> anyhow::Result<()> {
let file = NamedTempFile::new()?;
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let t0_id = store
.put_context_type("t0")
.property("p0", PropertyType::Int)
.execute()
.await?;
let t1_id = store.put_context_type("t1").execute().await?;
assert_ne!(t0_id, t1_id);
assert_eq!(
store.get_context_types().name("t0").execute().await?[0],
ContextType {
id: t0_id,
name: "t0".to_owned(),
properties: vec![("p0".to_owned(), PropertyType::Int)]
.into_iter()
.collect()
}
);
assert_eq!(
store.get_context_types().name("t1").execute().await?[0],
ContextType {
id: t1_id,
name: "t1".to_owned(),
properties: BTreeMap::new(),
}
);
assert!(store
.get_context_types()
.name("t2")
.execute()
.await?
.is_empty(),);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_context_types_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let types = store.get_context_types().execute().await?;
assert_eq!(types.len(), 1);
assert_eq!(types[0].name, "Experiment");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_attribution_works() -> anyhow::Result<()> {
let file = NamedTempFile::new().unwrap();
let mut store = MetadataStore::connect(&sqlite_uri(file.path()))
.await
.unwrap();
let t0 = store.put_artifact_type("t0").execute().await?;
let a0 = store.post_artifact(t0).execute().await?;
let _a1 = store.post_artifact(t0).execute().await?;
let t1 = store.put_context_type("t1").execute().await?;
let _c0 = store.post_context(t1, "foo").execute().await?;
let c1 = store.post_context(t1, "bar").execute().await?;
for _ in 0..2 {
store.put_attribution(c1, a0).execute().await?; let contexts = store.get_contexts().artifact(a0).execute().await?;
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].id, c1);
let artifacts = store.get_artifacts().context(c1).execute().await?;
assert_eq!(artifacts.len(), 1);
assert_eq!(artifacts[0].id, a0);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_association_works() -> anyhow::Result<()> {
let file = NamedTempFile::new().unwrap();
let mut store = MetadataStore::connect(&sqlite_uri(file.path()))
.await
.unwrap();
let t0 = store.put_execution_type("t0").execute().await?;
let e0 = store.post_execution(t0).execute().await?;
let _e1 = store.post_execution(t0).execute().await?;
let t1 = store.put_context_type("t1").execute().await?;
let _c0 = store.post_context(t1, "foo").execute().await?;
let c1 = store.post_context(t1, "bar").execute().await?;
for _ in 0..2 {
store.put_association(c1, e0).execute().await?; let contexts = store.get_contexts().execution(e0).execute().await?;
assert_eq!(contexts.len(), 1);
assert_eq!(contexts[0].id, c1);
let executions = store.get_executions().context(c1).execute().await?;
assert_eq!(executions.len(), 1);
assert_eq!(executions[0].id, e0);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn put_event_works() -> anyhow::Result<()> {
let file = NamedTempFile::new().unwrap();
let mut store = MetadataStore::connect(&sqlite_uri(file.path()))
.await
.unwrap();
let t0 = store.put_execution_type("t0").execute().await?;
let e0 = store.post_execution(t0).execute().await?;
let e1 = store.post_execution(t0).execute().await?;
let t1 = store.put_artifact_type("t1").execute().await?;
let a0 = store.post_artifact(t1).execute().await?;
let a1 = store.post_artifact(t1).execute().await?;
store
.put_event(e0, a0)
.ty(EventType::Input)
.execute()
.await?;
store
.put_event(e1, a1)
.ty(EventType::Output)
.step(EventStep::Index(30))
.execute()
.await?;
let events = store.get_events().execute().await?;
assert_eq!(events.len(), 2);
assert_eq!(events[0].artifact_id, a0);
assert_eq!(events[0].execution_id, e0);
assert_eq!(events[0].ty, EventType::Input);
assert_eq!(events[0].path, vec![]);
assert_eq!(events[1].artifact_id, a1);
assert_eq!(events[1].execution_id, e1);
assert_eq!(events[1].ty, EventType::Output);
assert_eq!(events[1].path, vec![EventStep::Index(30)]);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn get_events_works() -> anyhow::Result<()> {
let file = existing_db();
let mut store = MetadataStore::connect(&sqlite_uri(file.path())).await?;
let events = store.get_events().execute().await?;
assert_eq!(events, vec![event0(), event1()]);
let events = store
.get_events()
.artifact(ArtifactId::new(1))
.execute()
.await?;
assert_eq!(events, vec![event0()]);
let events = store
.get_events()
.artifact(ArtifactId::new(2))
.execute()
.await?;
assert_eq!(events, vec![event1()]);
let events = store
.get_events()
.execution(ExecutionId::new(1))
.execute()
.await?;
assert_eq!(events, vec![event0(), event1()]);
let events = store
.get_events()
.execution(ExecutionId::new(2))
.execute()
.await?;
assert_eq!(events, vec![]);
let events = store
.get_events()
.artifact(ArtifactId::new(1))
.execution(ExecutionId::new(1))
.execute()
.await?;
assert_eq!(events, vec![event0()]);
Ok(())
}
fn sqlite_uri(path: impl AsRef<std::path::Path>) -> String {
format!(
"sqlite://{}",
path.as_ref()
.to_str()
.ok_or_else(|| format!("invalid path: {:?}", path.as_ref()))
.unwrap()
)
}
fn existing_db() -> NamedTempFile {
let mut file = NamedTempFile::new().expect("cannot create a temporary file");
std::io::copy(
&mut std::fs::File::open("tests/test.db").expect("cannot open 'tests/test.db'"),
&mut file,
)
.expect("cannot copy the existing database file");
file
}
fn artifact0() -> Artifact {
Artifact {
id: ArtifactId::new(1),
type_id: TypeId::new(1),
name: None,
uri: Some("path/to/data".to_owned()),
properties: vec![
("day".to_owned(), PropertyValue::Int(1)),
(
"split".to_owned(),
PropertyValue::String("train".to_owned()),
),
]
.into_iter()
.collect(),
custom_properties: BTreeMap::new(),
state: ArtifactState::Unknown,
create_time_since_epoch: Duration::from_millis(1648979124872),
last_update_time_since_epoch: Duration::from_millis(1648979124872),
}
}
fn artifact1() -> Artifact {
Artifact {
id: ArtifactId::new(2),
type_id: TypeId::new(11),
name: None,
uri: Some("path/to/model/file".to_owned()),
properties: vec![
(
"name".to_owned(),
PropertyValue::String("MNIST-v1".to_owned()),
),
("version".to_owned(), PropertyValue::Int(1)),
]
.into_iter()
.collect(),
custom_properties: BTreeMap::new(),
state: ArtifactState::Unknown,
create_time_since_epoch: Duration::from_millis(1648979124885),
last_update_time_since_epoch: Duration::from_millis(1648979124885),
}
}
fn execution0() -> Execution {
Execution {
id: ExecutionId::new(1),
type_id: TypeId::new(12),
name: None,
last_known_state: ExecutionState::Unknown,
properties: vec![(
"state".to_owned(),
PropertyValue::String("COMPLETED".to_owned()),
)]
.into_iter()
.collect(),
custom_properties: BTreeMap::new(),
create_time_since_epoch: Duration::from_millis(1648979124878),
last_update_time_since_epoch: Duration::from_millis(1648979124891),
}
}
fn context0() -> Context {
Context {
id: ContextId::new(1),
type_id: TypeId::new(13),
name: "exp1".to_owned(),
properties: vec![(
"note".to_owned(),
PropertyValue::String("My first experiment.".to_owned()),
)]
.into_iter()
.collect(),
custom_properties: BTreeMap::new(),
create_time_since_epoch: Duration::from_millis(1648979124896),
last_update_time_since_epoch: Duration::from_millis(1648979124896),
}
}
fn event0() -> Event {
Event {
artifact_id: ArtifactId::new(1),
execution_id: ExecutionId::new(1),
path: Vec::new(),
ty: EventType::DeclaredInput,
create_time_since_epoch: Duration::from_millis(1648979124882),
}
}
fn event1() -> Event {
Event {
artifact_id: ArtifactId::new(2),
execution_id: ExecutionId::new(1),
path: Vec::new(),
ty: EventType::DeclaredOutput,
create_time_since_epoch: Duration::from_millis(1648979124888),
}
}