use serde::{Deserialize, Serialize};
use crate::spec::{
CollectionSpec, HnswConfigSpec, OptimizersConfigSpec, PayloadSchemaType, QuantizationSpec,
SparseVectorSpec, VectorSpec,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Operation {
CreateCollection { name: String, spec: CollectionSpec },
DeleteCollection { name: String },
UpdateCollection(UpdateCollectionOp),
CreateVector {
collection: String,
name: String,
spec: VectorSpec,
},
CreateSparseVector {
collection: String,
name: String,
spec: SparseVectorSpec,
},
DeleteVector { collection: String, name: String },
CreatePayloadIndex {
collection: String,
field_name: String,
schema: PayloadSchemaType,
},
DeletePayloadIndex {
collection: String,
field_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
schema: Option<PayloadSchemaType>,
},
CreateAlias { collection: String, alias: String },
DeleteAlias { alias: String },
SwitchAlias {
alias: String,
to_collection: String,
},
Exec(ExecOp),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UpdateCollectionOp {
pub collection: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hnsw_config: Option<HnswConfigSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quantization_config: Option<QuantizationSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub optimizers_config: Option<OptimizersConfigSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vectors: Option<indexmap::IndexMap<String, VectorParamsDiff>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VectorParamsDiff {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub on_disk: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hnsw_config: Option<HnswConfigSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quantization_config: Option<QuantizationSpec>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ExecOp {
pub command: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workdir: Option<String>,
}
#[derive(Debug, Clone)]
pub enum Reversibility {
Auto(Box<Operation>),
Irreversible(String),
}
impl Operation {
pub fn describe(&self) -> String {
match self {
Operation::CreateCollection { name, .. } => format!("create collection `{name}`"),
Operation::DeleteCollection { name } => format!("delete collection `{name}`"),
Operation::UpdateCollection(op) => {
format!("update config of collection `{}`", op.collection)
}
Operation::CreateVector {
collection, name, ..
} => format!("add vector `{name}` to `{collection}`"),
Operation::CreateSparseVector {
collection, name, ..
} => format!("add sparse vector `{name}` to `{collection}`"),
Operation::DeleteVector { collection, name } => {
format!("drop vector `{name}` from `{collection}`")
}
Operation::CreatePayloadIndex {
collection,
field_name,
..
} => format!("create index on `{collection}.{field_name}`"),
Operation::DeletePayloadIndex {
collection,
field_name,
..
} => format!("drop index on `{collection}.{field_name}`"),
Operation::CreateAlias { collection, alias } => {
format!("create alias `{alias}` -> `{collection}`")
}
Operation::DeleteAlias { alias } => format!("delete alias `{alias}`"),
Operation::SwitchAlias {
alias,
to_collection,
} => format!("switch alias `{alias}` -> `{to_collection}`"),
Operation::Exec(op) => {
format!(
"exec `{}`",
op.name.clone().unwrap_or_else(|| op.command.clone())
)
}
}
}
pub fn auto_inverse(&self) -> Reversibility {
match self {
Operation::CreateCollection { name, .. } => {
Reversibility::Auto(Box::new(Operation::DeleteCollection { name: name.clone() }))
}
Operation::DeleteCollection { name } => Reversibility::Irreversible(format!(
"deleting collection `{name}` destroys all its data; provide an explicit `down` to recreate it"
)),
Operation::UpdateCollection(op) => Reversibility::Irreversible(format!(
"config updates to `{}` are not auto-reversible (previous values unknown); provide an explicit `down`",
op.collection
)),
Operation::CreateVector {
collection, name, ..
} => Reversibility::Auto(Box::new(Operation::DeleteVector {
collection: collection.clone(),
name: name.clone(),
})),
Operation::CreateSparseVector {
collection, name, ..
} => Reversibility::Auto(Box::new(Operation::DeleteVector {
collection: collection.clone(),
name: name.clone(),
})),
Operation::DeleteVector { collection, name } => Reversibility::Irreversible(format!(
"dropping vector `{name}` from `{collection}` destroys its vector data; provide an explicit `down`"
)),
Operation::CreatePayloadIndex {
collection,
field_name,
schema,
} => Reversibility::Auto(Box::new(Operation::DeletePayloadIndex {
collection: collection.clone(),
field_name: field_name.clone(),
schema: Some(*schema),
})),
Operation::DeletePayloadIndex {
collection,
field_name,
schema,
} => match schema {
Some(schema) => Reversibility::Auto(Box::new(Operation::CreatePayloadIndex {
collection: collection.clone(),
field_name: field_name.clone(),
schema: *schema,
})),
None => Reversibility::Irreversible(format!(
"cannot recreate index on `{collection}.{field_name}` without its `schema`; add `schema:` to the op or provide an explicit `down`"
)),
},
Operation::CreateAlias { collection: _, alias } => {
Reversibility::Auto(Box::new(Operation::DeleteAlias {
alias: alias.clone(),
}))
}
Operation::DeleteAlias { alias } => Reversibility::Irreversible(format!(
"alias `{alias}`'s previous target is unknown; provide an explicit `down`"
)),
Operation::SwitchAlias { alias, .. } => Reversibility::Irreversible(format!(
"alias `{alias}`'s previous target is unknown; provide an explicit `down`"
)),
Operation::Exec(op) => Reversibility::Irreversible(format!(
"exec step `{}` has no automatic inverse; provide an explicit `down`",
op.name.clone().unwrap_or_else(|| op.command.clone())
)),
}
}
}