1use std::sync::Arc;
11
12use async_trait::async_trait;
13use uuid::Uuid;
14
15use khive_storage::error::StorageError;
16use khive_storage::event::{Event, EventFilter, EventObservation, ObservationRole, ReferentKind};
17use khive_storage::types::{BatchWriteSummary, Page, PageRequest, SqlStatement, SqlValue};
18use khive_storage::EventStore;
19use khive_storage::SqlWriter;
20use khive_storage::StorageCapability;
21use khive_types::{EventKind, EventOutcome, SubstrateKind};
22
23use crate::error::SqliteError;
24use crate::pool::ConnectionPool;
25use crate::writer_task::WriterTaskHandle;
26
27fn map_err(e: rusqlite::Error, op: &'static str) -> StorageError {
28 StorageError::driver(StorageCapability::Events, op, e)
29}
30
31fn map_sqlite_err(e: SqliteError, op: &'static str) -> StorageError {
32 StorageError::driver(StorageCapability::Events, op, e)
33}
34
35pub struct SqlEventStore {
37 pool: Arc<ConnectionPool>,
38 is_file_backed: bool,
39 namespace: String,
40 writer_task: Option<WriterTaskHandle>,
41}
42
43impl SqlEventStore {
44 pub fn new_scoped(
46 pool: Arc<ConnectionPool>,
47 is_file_backed: bool,
48 namespace: impl Into<String>,
49 ) -> Self {
50 let writer_task = pool.writer_task_handle().ok().flatten();
54 Self {
55 pool,
56 is_file_backed,
57 namespace: namespace.into(),
58 writer_task,
59 }
60 }
61
62 fn open_standalone_writer(&self) -> Result<rusqlite::Connection, StorageError> {
63 self.pool
64 .open_standalone_writer()
65 .map_err(|e| map_sqlite_err(e, "open_event_writer"))
66 }
67
68 fn open_standalone_reader(&self) -> Result<rusqlite::Connection, StorageError> {
69 self.pool
70 .open_standalone_reader()
71 .map_err(|e| map_sqlite_err(e, "open_event_reader"))
72 }
73
74 async fn with_writer<F, R>(&self, op: &'static str, f: F) -> Result<R, StorageError>
86 where
87 F: FnOnce(&rusqlite::Connection) -> Result<R, rusqlite::Error> + Send + 'static,
88 R: Send + 'static,
89 {
90 if let Some(writer_task) = &self.writer_task {
91 return writer_task
92 .send(move |conn| f(conn).map_err(|e| map_err(e, op)))
93 .await;
94 }
95
96 if self.is_file_backed {
97 let conn = self.open_standalone_writer()?;
98 tokio::task::spawn_blocking(move || f(&conn).map_err(|e| map_err(e, op)))
99 .await
100 .map_err(|e| StorageError::driver(StorageCapability::Events, op, e))?
101 } else {
102 let pool = Arc::clone(&self.pool);
103 tokio::task::spawn_blocking(move || {
104 let guard = pool.try_writer().map_err(|e| map_sqlite_err(e, op))?;
105 f(guard.conn()).map_err(|e| map_err(e, op))
106 })
107 .await
108 .map_err(|e| StorageError::driver(StorageCapability::Events, op, e))?
109 }
110 }
111
112 async fn with_reader<F, R>(&self, op: &'static str, f: F) -> Result<R, StorageError>
113 where
114 F: FnOnce(&rusqlite::Connection) -> Result<R, rusqlite::Error> + Send + 'static,
115 R: Send + 'static,
116 {
117 if self.is_file_backed {
118 let conn = self.open_standalone_reader()?;
119 tokio::task::spawn_blocking(move || f(&conn).map_err(|e| map_err(e, op)))
120 .await
121 .map_err(|e| StorageError::driver(StorageCapability::Events, op, e))?
122 } else {
123 let pool = Arc::clone(&self.pool);
124 tokio::task::spawn_blocking(move || {
125 let guard = pool.reader().map_err(|e| map_sqlite_err(e, op))?;
126 f(guard.conn()).map_err(|e| map_err(e, op))
127 })
128 .await
129 .map_err(|e| StorageError::driver(StorageCapability::Events, op, e))?
130 }
131 }
132}
133
134fn substrate_from_str(s: &str) -> Result<SubstrateKind, rusqlite::Error> {
139 s.parse::<SubstrateKind>().map_err(|_| {
140 rusqlite::Error::FromSqlConversionFailure(
141 0,
142 rusqlite::types::Type::Text,
143 format!("unknown SubstrateKind: {s}").into(),
144 )
145 })
146}
147
148fn outcome_from_str(s: &str) -> Result<EventOutcome, rusqlite::Error> {
149 match s {
150 "success" => Ok(EventOutcome::Success),
151 "denied" => Ok(EventOutcome::Denied),
152 "error" => Ok(EventOutcome::Error),
153 other => Err(rusqlite::Error::FromSqlConversionFailure(
154 0,
155 rusqlite::types::Type::Text,
156 format!("unknown EventOutcome: {other}").into(),
157 )),
158 }
159}
160
161fn kind_from_str(s: &str) -> Result<EventKind, rusqlite::Error> {
162 s.parse::<EventKind>().map_err(|_| {
163 rusqlite::Error::FromSqlConversionFailure(
164 0,
165 rusqlite::types::Type::Text,
166 format!("unknown EventKind: {s}").into(),
167 )
168 })
169}
170
171fn parse_uuid(s: &str) -> Result<Uuid, rusqlite::Error> {
172 Uuid::parse_str(s).map_err(|e| {
173 rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(e))
174 })
175}
176
177fn read_event(row: &rusqlite::Row<'_>) -> Result<Event, rusqlite::Error> {
182 let id_str: String = row.get(0)?;
183 let namespace: String = row.get(1)?;
184 let verb: String = row.get(2)?;
185 let substrate_str: String = row.get(3)?;
186 let actor: String = row.get(4)?;
187 let kind_str: String = row.get(5)?;
188 let outcome_str: String = row.get(6)?;
189 let payload_str: String = row.get(7)?;
190 let payload_schema_version: i64 = row.get(8)?;
191 let profile_state_version: Option<i64> = row.get(9)?;
192 let duration_us: i64 = row.get(10)?;
193 let target_str: Option<String> = row.get(11)?;
194 let session_str: Option<String> = row.get(12)?;
195 let aggregate_kind: Option<String> = row.get(13)?;
196 let aggregate_str: Option<String> = row.get(14)?;
197 let created_at: i64 = row.get(15)?;
198
199 let id = parse_uuid(&id_str)?;
200 let substrate = substrate_from_str(&substrate_str)?;
201 let kind = kind_from_str(&kind_str)?;
202 let outcome = outcome_from_str(&outcome_str)?;
203 let payload: serde_json::Value = serde_json::from_str(&payload_str).map_err(|e| {
204 rusqlite::Error::FromSqlConversionFailure(7, rusqlite::types::Type::Text, Box::new(e))
205 })?;
206 let target_id = target_str.as_deref().map(parse_uuid).transpose()?;
207 let session_id = session_str.as_deref().map(parse_uuid).transpose()?;
208 let aggregate_id = aggregate_str.as_deref().map(parse_uuid).transpose()?;
209 let payload_schema_version_u32: u32 = payload_schema_version.try_into().map_err(|_| {
210 rusqlite::Error::FromSqlConversionFailure(
211 8,
212 rusqlite::types::Type::Integer,
213 format!("payload_schema_version {payload_schema_version} out of u32 range").into(),
214 )
215 })?;
216 let profile_state_version_u64: Option<u64> = profile_state_version
217 .map(|v| {
218 u64::try_from(v).map_err(|_| {
219 rusqlite::Error::FromSqlConversionFailure(
220 9,
221 rusqlite::types::Type::Integer,
222 format!("profile_state_version {v} out of u64 range").into(),
223 )
224 })
225 })
226 .transpose()?;
227
228 Ok(Event {
229 id,
230 namespace,
231 verb,
232 substrate,
233 actor,
234 kind,
235 outcome,
236 payload,
237 payload_schema_version: payload_schema_version_u32,
238 profile_state_version: profile_state_version_u64,
239 duration_us,
240 target_id,
241 session_id,
242 aggregate_kind,
243 aggregate_id,
244 created_at,
245 })
246}
247
248fn insert_event_with_observations(
253 conn: &rusqlite::Connection,
254 event: &Event,
255) -> Result<(), rusqlite::Error> {
256 let id_str = event.id.to_string();
257 let substrate_str = event.substrate.name().to_string();
258 let kind_str = event.kind.name().to_string();
259 let outcome_str = event.outcome.name().to_string();
260 let payload_str = event.payload.to_string();
261 let target_str = event.target_id.map(|u| u.to_string());
262 let session_str = event.session_id.map(|u| u.to_string());
263 let aggregate_str = event.aggregate_id.map(|u| u.to_string());
264 let profile_state_version = event.profile_state_version.map(|v| v as i64);
265
266 conn.execute(
267 "INSERT INTO events \
268 (id, namespace, verb, substrate, actor, kind, outcome, payload, payload_schema_version, \
269 profile_state_version, duration_us, target_id, session_id, aggregate_kind, aggregate_id, created_at) \
270 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
271 rusqlite::params![
272 id_str,
273 &event.namespace,
274 &event.verb,
275 substrate_str,
276 &event.actor,
277 kind_str,
278 outcome_str,
279 payload_str,
280 event.payload_schema_version as i64,
281 profile_state_version,
282 event.duration_us,
283 target_str,
284 session_str,
285 &event.aggregate_kind,
286 aggregate_str,
287 event.created_at,
288 ],
289 )?;
290
291 for observation in decode_event_observations(event)? {
292 conn.execute(
293 "INSERT INTO event_observations \
294 (event_id, entity_id, referent_kind, role, position) \
295 VALUES (?1, ?2, ?3, ?4, ?5)",
296 rusqlite::params![
297 observation.event_id.to_string(),
298 observation.entity_id.to_string(),
299 observation.referent_kind.name(),
300 observation.role.name(),
301 observation.position as i64,
302 ],
303 )?;
304 }
305
306 Ok(())
307}
308
309fn batch_append_events_dml(
317 conn: &rusqlite::Connection,
318 events: &[Event],
319 attempted: u64,
320) -> Result<BatchWriteSummary, rusqlite::Error> {
321 let mut affected = 0u64;
322 for event in events {
323 insert_event_with_observations(conn, event)?;
324 affected += 1;
325 }
326 Ok(BatchWriteSummary {
327 attempted,
328 affected,
329 failed: 0,
330 first_error: String::new(),
331 })
332}
333
334pub fn event_insert_statements(event: &Event) -> Result<Vec<SqlStatement>, rusqlite::Error> {
345 let id_str = event.id.to_string();
346 let substrate_str = event.substrate.name().to_string();
347 let kind_str = event.kind.name().to_string();
348 let outcome_str = event.outcome.name().to_string();
349 let payload_str = event.payload.to_string();
350 let target_str = event.target_id.map(|u| u.to_string());
351 let session_str = event.session_id.map(|u| u.to_string());
352 let aggregate_str = event.aggregate_id.map(|u| u.to_string());
353 let profile_state_version = event.profile_state_version.map(|v| v as i64);
354
355 let mut statements = vec![SqlStatement {
356 sql: "INSERT INTO events \
357 (id, namespace, verb, substrate, actor, kind, outcome, payload, payload_schema_version, \
358 profile_state_version, duration_us, target_id, session_id, aggregate_kind, aggregate_id, created_at) \
359 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)"
360 .into(),
361 params: vec![
362 SqlValue::Text(id_str),
363 SqlValue::Text(event.namespace.clone()),
364 SqlValue::Text(event.verb.clone()),
365 SqlValue::Text(substrate_str),
366 SqlValue::Text(event.actor.clone()),
367 SqlValue::Text(kind_str),
368 SqlValue::Text(outcome_str),
369 SqlValue::Text(payload_str),
370 SqlValue::Integer(event.payload_schema_version as i64),
371 profile_state_version
372 .map(SqlValue::Integer)
373 .unwrap_or(SqlValue::Null),
374 SqlValue::Integer(event.duration_us),
375 target_str.map(SqlValue::Text).unwrap_or(SqlValue::Null),
376 session_str.map(SqlValue::Text).unwrap_or(SqlValue::Null),
377 event
378 .aggregate_kind
379 .clone()
380 .map(SqlValue::Text)
381 .unwrap_or(SqlValue::Null),
382 aggregate_str.map(SqlValue::Text).unwrap_or(SqlValue::Null),
383 SqlValue::Integer(event.created_at),
384 ],
385 label: Some("event_insert_on_writer".into()),
386 }];
387
388 for observation in decode_event_observations(event)? {
389 statements.push(SqlStatement {
390 sql: "INSERT INTO event_observations \
391 (event_id, entity_id, referent_kind, role, position) \
392 VALUES (?1, ?2, ?3, ?4, ?5)"
393 .into(),
394 params: vec![
395 SqlValue::Text(observation.event_id.to_string()),
396 SqlValue::Text(observation.entity_id.to_string()),
397 SqlValue::Text(observation.referent_kind.name().to_string()),
398 SqlValue::Text(observation.role.name().to_string()),
399 SqlValue::Integer(observation.position as i64),
400 ],
401 label: Some("event_observation_insert_on_writer".into()),
402 });
403 }
404
405 Ok(statements)
406}
407
408pub async fn append_event_on_writer(
427 writer: &mut dyn SqlWriter,
428 event: &Event,
429) -> Result<(), StorageError> {
430 let statements =
431 event_insert_statements(event).map_err(|e| map_err(e, "decode_event_observations"))?;
432 for statement in statements {
433 writer.execute(statement).await?;
434 }
435 Ok(())
436}
437
438fn decode_event_observations(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
439 match event.kind {
440 EventKind::RerankExecuted => decode_rerank_observations(event),
441 EventKind::RecallExecuted | EventKind::SearchExecuted => decode_recall_observations(event),
442 EventKind::LinkCreated => decode_link_observations(event),
443 EventKind::EntityCreated
444 | EventKind::EntityUpdated
445 | EventKind::EntityDeleted
446 | EventKind::NoteCreated
447 | EventKind::NoteUpdated
448 | EventKind::NoteDeleted
449 | EventKind::TaskTransitioned => decode_target_observation(event),
450 EventKind::FeedbackExplicit => decode_signal_observation(event),
451 _ => Ok(Vec::new()),
452 }
453}
454
455fn payload_uuid_array(event: &Event, field: &'static str) -> Result<Vec<Uuid>, rusqlite::Error> {
456 Ok(payload_uuid_array_opt(event, field)?.unwrap_or_default())
457}
458
459fn payload_uuid_array_opt(
466 event: &Event,
467 field: &'static str,
468) -> Result<Option<Vec<Uuid>>, rusqlite::Error> {
469 let Some(values) = event.payload.get(field) else {
470 return Ok(None);
471 };
472 let Some(array) = values.as_array() else {
473 return Err(invalid_payload(event.kind, field, "expected array"));
474 };
475
476 array
477 .iter()
478 .map(|value| {
479 value
480 .as_str()
481 .ok_or_else(|| invalid_payload(event.kind, field, "expected UUID string"))
482 .and_then(|s| Uuid::parse_str(s).map_err(|e| invalid_payload(event.kind, field, e)))
483 })
484 .collect::<Result<Vec<_>, _>>()
485 .map(Some)
486}
487
488fn payload_final_scores_uuid_array_opt(
496 event: &Event,
497 field: &'static str,
498) -> Result<Option<Vec<Uuid>>, rusqlite::Error> {
499 let Some(values) = event.payload.get(field) else {
500 return Ok(None);
501 };
502 let tuples: Vec<(khive_types::Id128, f32)> = serde_json::from_value(values.clone())
503 .map_err(|e| invalid_payload(event.kind, field, e))?;
504 tuples
505 .into_iter()
506 .map(|(id, score)| {
507 if !score.is_finite() {
508 return Err(invalid_payload(event.kind, field, "score is not finite"));
509 }
510 Ok(Uuid::from_bytes(*id.as_bytes()))
511 })
512 .collect::<Result<Vec<_>, _>>()
513 .map(Some)
514}
515
516fn payload_reranked_uuid_array_opt(
522 event: &Event,
523 field: &'static str,
524) -> Result<Option<Vec<Uuid>>, rusqlite::Error> {
525 let Some(values) = event.payload.get(field) else {
526 return Ok(None);
527 };
528 let tuples: Vec<(khive_types::Id128, Vec<(String, f32)>)> =
529 serde_json::from_value(values.clone())
530 .map_err(|e| invalid_payload(event.kind, field, e))?;
531 tuples
532 .into_iter()
533 .map(|(id, scores)| {
534 if !scores.iter().all(|(_, s)| s.is_finite()) {
535 return Err(invalid_payload(event.kind, field, "score is not finite"));
536 }
537 Ok(Uuid::from_bytes(*id.as_bytes()))
538 })
539 .collect::<Result<Vec<_>, _>>()
540 .map(Some)
541}
542
543fn payload_uuid(event: &Event, field: &'static str) -> Result<Option<Uuid>, rusqlite::Error> {
544 let Some(value) = event.payload.get(field) else {
545 return Ok(None);
546 };
547 let Some(s) = value.as_str() else {
548 return Err(invalid_payload(event.kind, field, "expected UUID string"));
549 };
550 Uuid::parse_str(s)
551 .map(Some)
552 .map_err(|e| invalid_payload(event.kind, field, e))
553}
554
555fn decode_candidate_observations(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
556 let mut rows = Vec::new();
557
558 for (position, entity_id) in payload_uuid_array(event, "candidates")?
559 .into_iter()
560 .enumerate()
561 {
562 let position_u32 = u32::try_from(position).map_err(|_| {
563 invalid_payload(
564 event.kind,
565 "candidates[position]",
566 "position out of u32 range",
567 )
568 })?;
569 rows.push(EventObservation {
570 event_id: event.id,
571 entity_id,
572 referent_kind: ReferentKind::Note,
573 role: ObservationRole::Candidate,
574 position: position_u32,
575 });
576 }
577
578 Ok(rows)
579}
580
581fn push_selected_observations(
582 event: &Event,
583 selected: Vec<Uuid>,
584 rows: &mut Vec<EventObservation>,
585) -> Result<(), rusqlite::Error> {
586 for (position, entity_id) in selected.into_iter().enumerate() {
587 let position_u32 = u32::try_from(position).map_err(|_| {
588 invalid_payload(
589 event.kind,
590 "selected[position]",
591 "position out of u32 range",
592 )
593 })?;
594 rows.push(EventObservation {
595 event_id: event.id,
596 entity_id,
597 referent_kind: ReferentKind::Note,
598 role: ObservationRole::Selected,
599 position: position_u32,
600 });
601 }
602 Ok(())
603}
604
605fn decode_recall_observations(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
609 let mut rows = decode_candidate_observations(event)?;
610 let selected = payload_uuid_array_opt(event, "selected")?.unwrap_or_default();
611 push_selected_observations(event, selected, &mut rows)?;
612 Ok(rows)
613}
614
615fn decode_rerank_observations(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
625 let mut rows = decode_candidate_observations(event)?;
626 let selected = payload_final_scores_uuid_array_opt(event, "final_scores")?
627 .or(payload_reranked_uuid_array_opt(event, "reranked")?)
628 .unwrap_or_default();
629 push_selected_observations(event, selected, &mut rows)?;
630
631 Ok(rows)
632}
633
634fn decode_link_observations(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
635 let mut rows = Vec::new();
636 if let Some(source) = payload_uuid(event, "source_id")? {
637 rows.push(EventObservation {
638 event_id: event.id,
639 entity_id: source,
640 referent_kind: ReferentKind::Entity,
641 role: ObservationRole::Target,
642 position: 0,
643 });
644 }
645 if let Some(target) = payload_uuid(event, "target_id")? {
646 rows.push(EventObservation {
647 event_id: event.id,
648 entity_id: target,
649 referent_kind: ReferentKind::Entity,
650 role: ObservationRole::Target,
651 position: 1,
652 });
653 }
654 Ok(rows)
655}
656
657fn decode_target_observation(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
658 let Some(entity_id) = event.target_id.or(payload_uuid(event, "target_id")?) else {
659 return Ok(Vec::new());
660 };
661 Ok(vec![EventObservation {
662 event_id: event.id,
663 entity_id,
664 referent_kind: if event.substrate == SubstrateKind::Note {
665 ReferentKind::Note
666 } else {
667 ReferentKind::Entity
668 },
669 role: ObservationRole::Target,
670 position: 0,
671 }])
672}
673
674fn decode_signal_observation(event: &Event) -> Result<Vec<EventObservation>, rusqlite::Error> {
675 let Some(entity_id) = event.target_id else {
676 return Ok(Vec::new());
677 };
678 Ok(vec![EventObservation {
679 event_id: event.id,
680 entity_id,
681 referent_kind: if event.substrate == SubstrateKind::Note {
687 ReferentKind::Note
688 } else {
689 ReferentKind::Entity
690 },
691 role: ObservationRole::Signal,
692 position: 0,
693 }])
694}
695
696fn invalid_payload(
697 kind: EventKind,
698 field: &'static str,
699 reason: impl std::fmt::Display,
700) -> rusqlite::Error {
701 rusqlite::Error::ToSqlConversionFailure(
702 format!("invalid payload for {}.{field}: {reason}", kind.name()).into(),
703 )
704}
705
706fn build_event_filter_sql(
711 conn: &rusqlite::Connection,
712 default_namespace: &str,
713 filter: &EventFilter,
714) -> Result<(String, Vec<Box<dyn rusqlite::types::ToSql>>), rusqlite::Error> {
715 reject_missing_event_filter_schema(conn, filter)?;
716
717 let mut conditions: Vec<String> = Vec::new();
718 let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
719
720 params.push(Box::new(default_namespace.to_string()));
721 conditions.push(format!("namespace = ?{}", params.len()));
722
723 push_in_clause(
724 &mut conditions,
725 &mut params,
726 "id",
727 filter.ids.iter().map(Uuid::to_string),
728 );
729 push_in_clause(
730 &mut conditions,
731 &mut params,
732 "kind",
733 filter.kinds.iter().map(|kind| kind.name().to_string()),
734 );
735 push_in_clause(
736 &mut conditions,
737 &mut params,
738 "verb",
739 filter.verbs.iter().cloned(),
740 );
741 push_in_clause(
742 &mut conditions,
743 &mut params,
744 "substrate",
745 filter.substrates.iter().map(|s| s.name().to_string()),
746 );
747 push_in_clause(
748 &mut conditions,
749 &mut params,
750 "actor",
751 filter.actors.iter().cloned(),
752 );
753
754 if let Some(after) = filter.after {
755 params.push(Box::new(after));
756 conditions.push(format!("created_at > ?{}", params.len()));
757 }
758
759 if let Some(before) = filter.before {
760 params.push(Box::new(before));
761 conditions.push(format!("created_at < ?{}", params.len()));
762 }
763
764 if let Some(session_id) = filter.session_id {
765 params.push(Box::new(session_id.to_string()));
766 conditions.push(format!("session_id = ?{}", params.len()));
767 }
768
769 push_observation_exists(&mut conditions, &mut params, "candidate", &filter.observed);
770 push_observation_exists(&mut conditions, &mut params, "selected", &filter.selected);
771
772 if let Some(proposal_id) = filter.payload_proposal_id {
773 params.push(Box::new(proposal_id.to_string()));
774 conditions.push(format!(
775 "json_extract(payload, '$.proposal_id') = ?{}",
776 params.len()
777 ));
778 }
779
780 let clause = format!(" WHERE {}", conditions.join(" AND "));
781 Ok((clause, params))
782}
783
784fn push_in_clause<I>(
785 conditions: &mut Vec<String>,
786 params: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
787 column: &'static str,
788 values: I,
789) where
790 I: IntoIterator<Item = String>,
791{
792 let placeholders: Vec<String> = values
793 .into_iter()
794 .map(|value| {
795 params.push(Box::new(value));
796 format!("?{}", params.len())
797 })
798 .collect();
799 if !placeholders.is_empty() {
800 conditions.push(format!("{column} IN ({})", placeholders.join(",")));
801 }
802}
803
804fn push_observation_exists(
805 conditions: &mut Vec<String>,
806 params: &mut Vec<Box<dyn rusqlite::types::ToSql>>,
807 role: &'static str,
808 entity_ids: &[Uuid],
809) {
810 if entity_ids.is_empty() {
811 return;
812 }
813 let placeholders: Vec<String> = entity_ids
814 .iter()
815 .map(|id| {
816 params.push(Box::new(id.to_string()));
817 format!("?{}", params.len())
818 })
819 .collect();
820 conditions.push(format!(
821 "EXISTS (SELECT 1 FROM event_observations o \
822 WHERE o.event_id = events.id AND o.role = '{role}' AND o.entity_id IN ({}))",
823 placeholders.join(",")
824 ));
825}
826
827fn reject_missing_event_filter_schema(
828 conn: &rusqlite::Connection,
829 filter: &EventFilter,
830) -> Result<(), rusqlite::Error> {
831 if filter.session_id.is_some() && !has_column(conn, "events", "session_id")? {
832 return Err(schema_absent("events.session_id"));
833 }
834 if (!filter.observed.is_empty() || !filter.selected.is_empty())
835 && !has_table(conn, "event_observations")?
836 {
837 return Err(schema_absent("event_observations"));
838 }
839 if filter.payload_proposal_id.is_some() && !has_column(conn, "events", "payload")? {
840 return Err(schema_absent("events.payload"));
841 }
842 Ok(())
843}
844
845fn has_table(conn: &rusqlite::Connection, table: &'static str) -> Result<bool, rusqlite::Error> {
846 conn.query_row(
847 "SELECT COUNT(*) > 0 FROM sqlite_master WHERE type = 'table' AND name = ?1",
848 [table],
849 |row| row.get(0),
850 )
851}
852
853fn has_column(
854 conn: &rusqlite::Connection,
855 table: &'static str,
856 column: &'static str,
857) -> Result<bool, rusqlite::Error> {
858 conn.query_row(
859 "SELECT COUNT(*) > 0 FROM pragma_table_info(?1) WHERE name = ?2",
860 rusqlite::params![table, column],
861 |row| row.get(0),
862 )
863}
864
865fn schema_absent(name: &'static str) -> rusqlite::Error {
866 rusqlite::Error::ToSqlConversionFailure(
867 format!("event filter requires missing schema element {name}; run migrations").into(),
868 )
869}
870
871#[async_trait]
876impl EventStore for SqlEventStore {
877 async fn append_event(&self, event: Event) -> Result<(), StorageError> {
878 if let Some(writer_task) = &self.writer_task {
883 return writer_task
884 .send(move |conn| {
885 insert_event_with_observations(conn, &event)
886 .map_err(|e| map_err(e, "append_event"))
887 })
888 .await;
889 }
890
891 self.with_writer("append_event", move |conn| {
894 conn.execute_batch("BEGIN IMMEDIATE")?;
895 let _tx_handle = khive_storage::tx_registry::register(Some("event_append".to_string()));
896 if let Err(e) = insert_event_with_observations(conn, &event) {
897 let _ = conn.execute_batch("ROLLBACK");
898 return Err(e);
899 }
900 conn.execute_batch("COMMIT")?;
901 Ok(())
902 })
903 .await
904 }
905
906 async fn append_events(&self, events: Vec<Event>) -> Result<BatchWriteSummary, StorageError> {
907 let attempted = events.len() as u64;
908
909 if let Some(writer_task) = &self.writer_task {
915 return writer_task
916 .send(move |conn| {
917 batch_append_events_dml(conn, &events, attempted)
918 .map_err(|e| map_err(e, "append_events"))
919 })
920 .await;
921 }
922
923 self.with_writer("append_events", move |conn| {
926 conn.execute_batch("BEGIN IMMEDIATE")?;
927 let _tx_handle =
928 khive_storage::tx_registry::register(Some("event_append_batch".to_string()));
929
930 let summary = match batch_append_events_dml(conn, &events, attempted) {
931 Ok(summary) => summary,
932 Err(e) => {
933 let _ = conn.execute_batch("ROLLBACK");
934 return Err(e);
935 }
936 };
937
938 conn.execute_batch("COMMIT")?;
939 Ok(summary)
940 })
941 .await
942 }
943
944 async fn get_event(&self, id: Uuid) -> Result<Option<Event>, StorageError> {
945 let namespace = self.namespace.clone();
946 let id_str = id.to_string();
947
948 self.with_reader("get_event", move |conn| {
949 let mut stmt = conn.prepare(
950 "SELECT id, namespace, verb, substrate, actor, kind, outcome, payload, \
951 payload_schema_version, profile_state_version, duration_us, target_id, \
952 session_id, aggregate_kind, aggregate_id, created_at \
953 FROM events WHERE namespace = ?1 AND id = ?2",
954 )?;
955 let mut rows = stmt.query(rusqlite::params![namespace, id_str])?;
956 match rows.next()? {
957 Some(row) => Ok(Some(read_event(row)?)),
958 None => Ok(None),
959 }
960 })
961 .await
962 }
963
964 async fn query_events(
965 &self,
966 filter: EventFilter,
967 page: PageRequest,
968 ) -> Result<Page<Event>, StorageError> {
969 let namespace = self.namespace.clone();
970 let limit_i64 = i64::from(page.limit);
971 let offset_i64 = i64::try_from(page.offset).map_err(|_| StorageError::InvalidInput {
972 capability: StorageCapability::Events,
973 operation: "query_events".into(),
974 message: format!(
975 "PageRequest: offset must be <= i64::MAX, got {}",
976 page.offset
977 ),
978 })?;
979
980 self.with_reader("query_events", move |conn| {
981 let (where_clause, filter_params) = build_event_filter_sql(conn, &namespace, &filter)?;
982
983 let count_sql = format!("SELECT COUNT(*) FROM events{}", where_clause);
984 let total: i64 = {
985 let mut stmt = conn.prepare(&count_sql)?;
986 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
987 filter_params.iter().map(|p| p.as_ref()).collect();
988 stmt.query_row(param_refs.as_slice(), |row| row.get(0))?
989 };
990
991 let (_, data_filter_params) = build_event_filter_sql(conn, &namespace, &filter)?;
992 let mut all_params: Vec<Box<dyn rusqlite::types::ToSql>> = data_filter_params;
993 all_params.push(Box::new(limit_i64));
994 all_params.push(Box::new(offset_i64));
995
996 let limit_idx = all_params.len() - 1;
997 let offset_idx = all_params.len();
998
999 let data_sql = format!(
1000 "SELECT id, namespace, verb, substrate, actor, kind, outcome, payload, \
1001 payload_schema_version, profile_state_version, duration_us, target_id, \
1002 session_id, aggregate_kind, aggregate_id, created_at \
1003 FROM events{} ORDER BY created_at DESC, id DESC LIMIT ?{} OFFSET ?{}",
1004 where_clause, limit_idx, offset_idx,
1005 );
1006
1007 let mut stmt = conn.prepare(&data_sql)?;
1008 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
1009 all_params.iter().map(|p| p.as_ref()).collect();
1010 let rows = stmt.query_map(param_refs.as_slice(), read_event)?;
1011
1012 let mut items = Vec::new();
1013 for row in rows {
1014 items.push(row?);
1015 }
1016
1017 Ok(Page {
1018 items,
1019 total: Some(total as u64),
1020 })
1021 })
1022 .await
1023 }
1024
1025 async fn count_events(&self, filter: EventFilter) -> Result<u64, StorageError> {
1026 let namespace = self.namespace.clone();
1027
1028 self.with_reader("count_events", move |conn| {
1029 let (where_clause, params) = build_event_filter_sql(conn, &namespace, &filter)?;
1030 let sql = format!("SELECT COUNT(*) FROM events{}", where_clause);
1031 let mut stmt = conn.prepare(&sql)?;
1032 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
1033 params.iter().map(|p| p.as_ref()).collect();
1034 let count: i64 = stmt.query_row(param_refs.as_slice(), |row| row.get(0))?;
1035 Ok(count as u64)
1036 })
1037 .await
1038 }
1039}
1040
1041const EVENTS_DDL: &str = include_str!("../../sql/events-ddl.sql");
1046
1047pub(crate) fn ensure_events_schema(conn: &rusqlite::Connection) -> Result<(), rusqlite::Error> {
1048 conn.execute_batch(EVENTS_DDL)
1049}
1050
1051#[cfg(test)]
1052#[path = "event_tests.rs"]
1053mod tests;