use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::api::enrichments::{Coverage, Definition, NativeState, Payload};
use crate::api::tasks::{
EnrichmentRefreshDefinition, TaskDefinition, TaskReadTarget, TaskReadTargetCommitOnBranch,
};
use crate::catalog_types::{ProjectBranchId, ProjectCommitId};
use crate::{Error, Expression, Project, ResourceAddress, Run, Snapshot, TaskHandle};
pub(crate) const ENRICHMENT_KIND: &str = "enrichment";
const PRODUCER_CODEC: &str = "scandal.expression";
const OUTPUT_CODEC: &str = "arrow.field";
const COVERAGE_CODEC: &str = "spiral.enrichment-coverage";
const MIRROR_CODEC: &str = "scandal.mirror";
const DEFINITION_VERSION: u32 = 1;
pub const ENRICHMENT_VALUE_COLUMN: &str = "value";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnrichmentDefinition {
pub expression: Expression,
pub output: arrow_schema::Field,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Producer {
expression: Expression,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct EnrichmentCoverage {
computed: bool,
}
#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct Mirror {}
impl EnrichmentDefinition {
pub fn to_wire(&self) -> Result<Definition, Error> {
Ok(Definition {
schema_version: DEFINITION_VERSION,
output_dtype: payload(OUTPUT_CODEC, &self.output)?,
dependency_field_ids: Vec::new(),
producer: payload(
PRODUCER_CODEC,
&Producer {
expression: self.expression.clone(),
},
)?,
})
}
pub fn from_wire(definition: &Definition) -> Result<Self, Error> {
if definition.schema_version != DEFINITION_VERSION {
return Err(Error::invalid(format!(
"unsupported Enrichment definition version {}",
definition.schema_version
)));
}
let producer: Producer = decode(PRODUCER_CODEC, &definition.producer)?;
Ok(Self {
expression: producer.expression,
output: decode(OUTPUT_CODEC, &definition.output_dtype)?,
})
}
}
pub fn enrichment_coverage(computed: bool) -> Result<Coverage, Error> {
Ok(Coverage {
state: payload(COVERAGE_CODEC, &EnrichmentCoverage { computed })?,
})
}
pub fn mirror_state(backing_version_id: Uuid) -> Result<NativeState, Error> {
Ok(NativeState {
backing_version_id,
state: payload(MIRROR_CODEC, &Mirror {})?,
})
}
pub fn mirrored_version(state: &NativeState) -> Result<Uuid, Error> {
let Mirror {} = decode(MIRROR_CODEC, &state.state)?;
Ok(state.backing_version_id)
}
fn payload(codec: &str, value: &impl Serialize) -> Result<Payload, Error> {
Ok(Payload {
codec: codec.to_owned(),
codec_version: 1,
canonical_payload: serde_json::to_string(value).map_err(Error::invalid)?,
})
}
fn decode<T: serde::de::DeserializeOwned>(codec: &str, payload: &Payload) -> Result<T, Error> {
if payload.codec != codec || payload.codec_version != 1 {
return Err(Error::invalid(format!(
"unsupported Enrichment payload `{}` version {}, expected `{codec}` version 1",
payload.codec, payload.codec_version
)));
}
serde_json::from_str(&payload.canonical_payload).map_err(Error::invalid)
}
#[derive(Clone)]
pub struct Enrichment {
project: Project,
commit_id: ProjectCommitId,
branch_id: Option<ProjectBranchId>,
resource_id: Uuid,
version_id: Uuid,
}
impl Snapshot {
pub async fn enrichment(&self, path: &str) -> Result<Enrichment, Error> {
let address = path.parse::<ResourceAddress>()?;
if address.reference().is_some() {
return Err(Error::invalid(
"select the Enrichment's branch with the snapshot, not an `@` reference",
));
}
let resource = self.resource_at_path(address.path()).await?;
if resource.descriptor.kind.0 != ENRICHMENT_KIND {
return Err(Error::invalid(format!("`{path}` is not an Enrichment")));
}
Ok(Enrichment {
project: self.project.clone(),
commit_id: self.commit_id,
branch_id: self.branch_id,
resource_id: resource.descriptor.resource_id.as_uuid(),
version_id: resource.version_id.as_uuid(),
})
}
}
impl Enrichment {
pub fn resource_id(&self) -> Uuid {
self.resource_id
}
pub fn version_id(&self) -> Uuid {
self.version_id
}
pub async fn refresh(&self, run: Run) -> Result<Option<TaskHandle>, Error> {
let branch_id = self.branch_id.ok_or_else(|| {
Error::invalid("refreshing an Enrichment needs a branch snapshot, not an exact commit")
})?;
let definition = TaskDefinition::EnrichmentRefresh(EnrichmentRefreshDefinition {
read_target: TaskReadTarget::CommitOnBranch(TaskReadTargetCommitOnBranch {
branch_id: branch_id.as_uuid(),
commit_id: self.commit_id.as_uuid(),
}),
enrichment_resource_id: self.resource_id,
});
self.project.submit_task(definition, None, run).await
}
}