use super::*;
use crate::secret_gate_finalizer::entity_admission::{
prepare_entity_admission_at, EntityAdmission, EntityEntryPoint,
};
use crate::EntityCandidateMutation;
impl KhiveRuntime {
pub async fn create_many(
&self,
token: &NamespaceToken,
specs: Vec<EntityCreateSpec>,
) -> RuntimeResult<Vec<Entity>> {
if specs.is_empty() {
return Ok(vec![]);
}
let ns = token.namespace().as_str();
let mut admissions = Vec::with_capacity(specs.len());
for (index, spec) in specs.iter().enumerate() {
let entity = self.validate_bulk_entity(ns, spec)?;
admissions.push(prepare_entity_admission_at(
token,
EntityEntryPoint::Bulk,
entity,
&format!("entity[{index}]"),
)?);
}
#[cfg(any(test, feature = "fault-injection"))]
let fts_many_inject = consume_fault(&FTS_FAIL_MANY_NS, ns);
#[cfg(not(any(test, feature = "fault-injection")))]
let fts_many_inject = false;
#[cfg(any(test, feature = "fault-injection"))]
let fts_many_inject_partial = consume_fault(&FTS_FAIL_MANY_PARTIAL_NS, ns);
#[cfg(not(any(test, feature = "fault-injection")))]
let fts_many_inject_partial = false;
let injected_failure_index = if fts_many_inject {
Some(0)
} else if fts_many_inject_partial {
Some(usize::from(admissions.len() > 1))
} else {
None
};
let _ = self.entities(token)?;
let _ = self.text(token)?;
let mut entities = Vec::with_capacity(admissions.len());
let mut plans = Vec::with_capacity(admissions.len());
let mut has_finalizer = false;
for (index, admission) in admissions.into_iter().enumerate() {
has_finalizer |= matches!(&admission, EntityAdmission::Exempt(_));
let (entity, plan) = self
.finish_bulk_entity_plan(token, admission, injected_failure_index == Some(index))
.await?;
entities.push(entity);
plans.push(plan);
}
match run_atomic_unit(self.sql().as_ref(), plans).await {
Ok(AtomicRunOutcome::Committed { .. }) => Ok(entities),
Ok(AtomicRunOutcome::RolledBack {
failed_op_index,
failure,
}) => Err(RuntimeError::Internal(format!(
"create_many: atomic batch rolled back at entity index {failed_op_index}: \
{failure:?}"
))),
Err(e) if has_finalizer => Err(RuntimeError::Storage(e.0)),
Err(e) => Err(RuntimeError::Internal(format!(
"create_many: atomic batch failed: {}",
e.0
))),
}
}
fn validate_bulk_entity(&self, ns: &str, spec: &EntityCreateSpec) -> RuntimeResult<Entity> {
self.validate_entity_kind(&spec.kind)?;
let validated_type =
self.validate_entity_type_for_kind(&spec.kind, spec.entity_type.as_deref())?;
if spec.name.trim().is_empty() {
return Err(RuntimeError::InvalidInput("name must not be empty".into()));
}
crate::secret_gate::reject_reserved_secret_gate_property(spec.properties.as_ref())?;
let mut entity =
Entity::new(ns, &spec.kind, &spec.name).with_entity_type(validated_type.as_deref());
if let Some(d) = &spec.description {
entity = entity.with_description(d);
}
if let Some(p) = spec.properties.clone() {
entity = entity.with_properties(p);
}
if !spec.tags.is_empty() {
entity = entity.with_tags(spec.tags.clone());
}
Ok(entity)
}
pub async fn prepare_bulk_entity_plan(
&self,
token: &NamespaceToken,
spec: EntityCreateSpec,
) -> RuntimeResult<(Entity, AtomicOpPlan)> {
let entity = self.validate_bulk_entity(token.namespace().as_str(), &spec)?;
let admission =
prepare_entity_admission_at(token, EntityEntryPoint::Bulk, entity, "entity")?;
let _ = self.entities(token)?;
let _ = self.text(token)?;
self.finish_bulk_entity_plan(token, admission, false).await
}
async fn finish_bulk_entity_plan(
&self,
token: &NamespaceToken,
admission: EntityAdmission,
inject_fts_failure: bool,
) -> RuntimeResult<(Entity, AtomicOpPlan)> {
match admission {
EntityAdmission::Legacy(entity) => {
let mut plan = bulk_entity_plan(&entity)?;
if inject_fts_failure {
plan.statements.truncate(1);
plan.statements.push(bulk_fts_failure());
}
Ok((entity, AtomicOpPlan::AddEntity(plan)))
}
EntityAdmission::Exempt(prepared) => {
let entity = prepared.entity().clone();
let (mut required, _) = self
.prepare_admitted_entity_indexes(token, &entity, &[], false)
.await?;
if inject_fts_failure {
required = vec![bulk_fts_failure()];
}
let plan = prepared.into_plan(
EntityCandidateMutation::CreateIfAbsent,
required,
PostCommitEffect::None,
)?;
Ok((entity, AtomicOpPlan::FinalizeEntity(Box::new(plan))))
}
}
}
}
fn bulk_entity_plan(entity: &Entity) -> RuntimeResult<AddEntityPlan> {
crate::secret_gate::reject_reserved_secret_gate_property(entity.properties.as_ref())?;
let mut statements = vec![PlanStatement {
statement: entity_upsert_statement(entity),
guard: Some(AffectedRowGuard::exactly(1)),
}];
statements.extend(
insert_document_statements("fts_entities", &entity_fts_document(entity))
.into_iter()
.map(|statement| PlanStatement {
statement,
guard: None,
}),
);
Ok(AddEntityPlan {
entity_id: entity.id,
statements,
post_commit: PostCommitEffect::None,
})
}
fn bulk_fts_failure() -> PlanStatement {
PlanStatement {
statement: SqlStatement {
sql: "INSERT INTO __khive_create_many_injected_failure__ DEFAULT VALUES".into(),
params: vec![],
label: Some("fts-insert-injected-failure".into()),
},
guard: None,
}
}