khive_runtime/operations/
entity_bulk.rs1use super::*;
4use crate::secret_gate_finalizer::entity_admission::{
5 prepare_entity_admission_at, EntityAdmission, EntityEntryPoint,
6};
7use crate::EntityCandidateMutation;
8
9impl KhiveRuntime {
10 pub async fn create_many(
21 &self,
22 token: &NamespaceToken,
23 specs: Vec<EntityCreateSpec>,
24 ) -> RuntimeResult<Vec<Entity>> {
25 if specs.is_empty() {
26 return Ok(vec![]);
27 }
28 let ns = token.namespace().as_str();
29
30 let mut admissions = Vec::with_capacity(specs.len());
34 for (index, spec) in specs.iter().enumerate() {
35 let entity = self.validate_bulk_entity(ns, spec)?;
36 admissions.push(prepare_entity_admission_at(
37 token,
38 EntityEntryPoint::Bulk,
39 entity,
40 &format!("entity[{index}]"),
41 )?);
42 }
43
44 #[cfg(any(test, feature = "fault-injection"))]
45 let fts_many_inject = consume_fault(&FTS_FAIL_MANY_NS, ns);
46 #[cfg(not(any(test, feature = "fault-injection")))]
47 let fts_many_inject = false;
48
49 #[cfg(any(test, feature = "fault-injection"))]
50 let fts_many_inject_partial = consume_fault(&FTS_FAIL_MANY_PARTIAL_NS, ns);
51 #[cfg(not(any(test, feature = "fault-injection")))]
52 let fts_many_inject_partial = false;
53
54 let injected_failure_index = if fts_many_inject {
55 Some(0)
56 } else if fts_many_inject_partial {
57 Some(usize::from(admissions.len() > 1))
58 } else {
59 None
60 };
61
62 let _ = self.entities(token)?;
63 let _ = self.text(token)?;
64
65 let mut entities = Vec::with_capacity(admissions.len());
66 let mut plans = Vec::with_capacity(admissions.len());
67 let mut has_finalizer = false;
68 for (index, admission) in admissions.into_iter().enumerate() {
69 has_finalizer |= matches!(&admission, EntityAdmission::Exempt(_));
70 let (entity, plan) = self
71 .finish_bulk_entity_plan(token, admission, injected_failure_index == Some(index))
72 .await?;
73 entities.push(entity);
74 plans.push(plan);
75 }
76
77 match run_atomic_unit(self.sql().as_ref(), plans).await {
78 Ok(AtomicRunOutcome::Committed { .. }) => Ok(entities),
79 Ok(AtomicRunOutcome::RolledBack {
80 failed_op_index,
81 failure,
82 }) => Err(RuntimeError::Internal(format!(
83 "create_many: atomic batch rolled back at entity index {failed_op_index}: \
84 {failure:?}"
85 ))),
86 Err(e) if has_finalizer => Err(RuntimeError::Storage(e.0)),
87 Err(e) => Err(RuntimeError::Internal(format!(
88 "create_many: atomic batch failed: {}",
89 e.0
90 ))),
91 }
92 }
93
94 fn validate_bulk_entity(&self, ns: &str, spec: &EntityCreateSpec) -> RuntimeResult<Entity> {
99 self.validate_entity_kind(&spec.kind)?;
100 let validated_type =
105 self.validate_entity_type_for_kind(&spec.kind, spec.entity_type.as_deref())?;
106 if spec.name.trim().is_empty() {
107 return Err(RuntimeError::InvalidInput("name must not be empty".into()));
108 }
109 crate::secret_gate::reject_reserved_secret_gate_property(spec.properties.as_ref())?;
110
111 let mut entity =
112 Entity::new(ns, &spec.kind, &spec.name).with_entity_type(validated_type.as_deref());
113 if let Some(d) = &spec.description {
114 entity = entity.with_description(d);
115 }
116 if let Some(p) = spec.properties.clone() {
117 entity = entity.with_properties(p);
118 }
119 if !spec.tags.is_empty() {
120 entity = entity.with_tags(spec.tags.clone());
121 }
122 Ok(entity)
123 }
124
125 pub async fn prepare_bulk_entity_plan(
132 &self,
133 token: &NamespaceToken,
134 spec: EntityCreateSpec,
135 ) -> RuntimeResult<(Entity, AtomicOpPlan)> {
136 let entity = self.validate_bulk_entity(token.namespace().as_str(), &spec)?;
137 let admission =
138 prepare_entity_admission_at(token, EntityEntryPoint::Bulk, entity, "entity")?;
139 let _ = self.entities(token)?;
140 let _ = self.text(token)?;
141 self.finish_bulk_entity_plan(token, admission, false).await
142 }
143
144 async fn finish_bulk_entity_plan(
145 &self,
146 token: &NamespaceToken,
147 admission: EntityAdmission,
148 inject_fts_failure: bool,
149 ) -> RuntimeResult<(Entity, AtomicOpPlan)> {
150 match admission {
151 EntityAdmission::Legacy(entity) => {
152 let mut plan = bulk_entity_plan(&entity)?;
153 if inject_fts_failure {
154 plan.statements.truncate(1);
155 plan.statements.push(bulk_fts_failure());
156 }
157 Ok((entity, AtomicOpPlan::AddEntity(plan)))
158 }
159 EntityAdmission::Exempt(prepared) => {
160 let entity = prepared.entity().clone();
161 let (mut required, _) = self
162 .prepare_admitted_entity_indexes(token, &entity, &[], false)
163 .await?;
164 if inject_fts_failure {
165 required = vec![bulk_fts_failure()];
166 }
167 let plan = prepared.into_plan(
168 EntityCandidateMutation::CreateIfAbsent,
169 required,
170 PostCommitEffect::None,
171 )?;
172 Ok((entity, AtomicOpPlan::FinalizeEntity(Box::new(plan))))
173 }
174 }
175 }
176}
177
178fn bulk_entity_plan(entity: &Entity) -> RuntimeResult<AddEntityPlan> {
179 crate::secret_gate::reject_reserved_secret_gate_property(entity.properties.as_ref())?;
180 let mut statements = vec![PlanStatement {
181 statement: entity_upsert_statement(entity),
182 guard: Some(AffectedRowGuard::exactly(1)),
183 }];
184 statements.extend(
186 insert_document_statements("fts_entities", &entity_fts_document(entity))
187 .into_iter()
188 .map(|statement| PlanStatement {
189 statement,
190 guard: None,
191 }),
192 );
193 Ok(AddEntityPlan {
194 entity_id: entity.id,
195 statements,
196 post_commit: PostCommitEffect::None,
197 })
198}
199
200fn bulk_fts_failure() -> PlanStatement {
201 PlanStatement {
202 statement: SqlStatement {
203 sql: "INSERT INTO __khive_create_many_injected_failure__ DEFAULT VALUES".into(),
204 params: vec![],
205 label: Some("fts-insert-injected-failure".into()),
206 },
207 guard: None,
208 }
209}