spectra_backend_tensorbase/events.rs
1//! TensorBase events storage adapter.
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use serde_json::Value;
6use spectra_backend_remote_common::RemoteEventsBackend;
7use spectra_core::{
8 EventAggregateResult, EventRow, EventStorageBackend, EventWriteRow, EventsAggregateFilter,
9 EventsQueryFilter, Result, StorageEngineType,
10};
11
12/// Scale-out TensorBase structured-event storage over its ClickHouse-compatible protocol.
13///
14/// Use [`connect`](Self::connect) with a full protocol URL or
15/// [`connect_host`](Self::connect_host) for the default native port (`9528`). Pair this type
16/// with `TensorBaseMetricsBackend` when building the runtime.
17///
18/// # Examples
19///
20/// Facade wiring through `Spectra::builder()` (requires the `spectra` crate with the
21/// `tensorbase` feature):
22///
23/// ```ignore
24/// use std::sync::Arc;
25/// use spectra::{Spectra, TensorBaseEventsBackend, TensorBaseMetricsBackend};
26///
27/// # async fn start() -> spectra::Result<()> {
28/// let url = "tcp://127.0.0.1:9528";
29/// let spectra = Spectra::builder()
30/// .metrics_backend(Arc::new(TensorBaseMetricsBackend::connect(url).await?))
31/// .events_backend(Arc::new(TensorBaseEventsBackend::connect(url).await?))
32/// .build()?;
33/// # let _ = spectra;
34/// # Ok(())
35/// # }
36/// ```
37pub struct TensorBaseEventsBackend(RemoteEventsBackend);
38
39impl TensorBaseEventsBackend {
40 /// Connect with a full TensorBase protocol URL and ensure event tables exist.
41 ///
42 /// Accepts `tcp://host:9528` (native) or an HTTP ClickHouse-compatible endpoint. The call
43 /// is async and executes DDL.
44 ///
45 /// # Examples
46 ///
47 /// ```ignore
48 /// # async fn example() -> spectra_core::Result<()> {
49 /// use spectra_backend_tensorbase::TensorBaseEventsBackend;
50 ///
51 /// let backend = TensorBaseEventsBackend::connect("tcp://127.0.0.1:9528").await?;
52 /// # let _ = backend;
53 /// # Ok(())
54 /// # }
55 /// ```
56 pub async fn connect(url: &str) -> Result<Self> {
57 Ok(Self(
58 RemoteEventsBackend::connect(
59 url,
60 StorageEngineType::TensorBase,
61 &crate::ddl::events_ddl(),
62 )
63 .await?,
64 ))
65 }
66
67 /// Connect using a host name and the default native port (`9528`).
68 ///
69 /// Equivalent to [`connect`](Self::connect) with `tcp://{host}:9528`.
70 ///
71 /// # Examples
72 ///
73 /// ```ignore
74 /// # async fn example() -> spectra_core::Result<()> {
75 /// use spectra_backend_tensorbase::TensorBaseEventsBackend;
76 ///
77 /// let backend = TensorBaseEventsBackend::connect_host("127.0.0.1").await?;
78 /// # let _ = backend;
79 /// # Ok(())
80 /// # }
81 /// ```
82 pub async fn connect_host(host: &str) -> Result<Self> {
83 Self::connect(&crate::ddl::default_url(host)).await
84 }
85
86 /// In-memory stub that preserves the TensorBase engine type for storage-contract tests.
87 ///
88 /// # Examples
89 ///
90 /// ```
91 /// use spectra_backend_tensorbase::TensorBaseEventsBackend;
92 /// use spectra_core::{EventStorageBackend, StorageEngineType};
93 ///
94 /// let backend = TensorBaseEventsBackend::in_memory_stub();
95 /// assert_eq!(backend.engine_type(), StorageEngineType::TensorBase);
96 /// ```
97 pub fn in_memory_stub() -> Self {
98 Self(RemoteEventsBackend::in_memory_for_test(
99 StorageEngineType::TensorBase,
100 ))
101 }
102
103 /// In-memory stub for unit tests in this crate.
104 #[cfg(test)]
105 pub fn in_memory_for_test() -> Self {
106 Self::in_memory_stub()
107 }
108}
109
110#[async_trait]
111impl EventStorageBackend for TensorBaseEventsBackend {
112 fn engine_type(&self) -> StorageEngineType {
113 self.0.engine_type()
114 }
115
116 async fn append_row(
117 &self,
118 table: &str,
119 fields: &Value,
120 ts: DateTime<Utc>,
121 correlation_id: Option<&str>,
122 ) -> Result<()> {
123 self.0.append_row(table, fields, ts, correlation_id).await
124 }
125
126 async fn append_rows_batch(&self, rows: &[EventWriteRow]) -> Result<()> {
127 self.0.append_rows_batch(rows).await
128 }
129
130 async fn query_rows(&self, filter: EventsQueryFilter) -> Result<Vec<EventRow>> {
131 self.0.query_rows(filter).await
132 }
133
134 async fn query_aggregate(&self, filter: EventsAggregateFilter) -> Result<EventAggregateResult> {
135 self.0.query_aggregate(filter).await
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use serde_json::json;
143
144 #[tokio::test]
145 async fn tensorbase_events_roundtrip_in_memory() {
146 let backend = TensorBaseEventsBackend::in_memory_for_test();
147 let ts = Utc::now();
148 backend
149 .append_row("req", &json!({"x": 1}), ts, None)
150 .await
151 .expect("write");
152 let rows = backend
153 .query_rows(EventsQueryFilter {
154 table: "req".into(),
155 ..Default::default()
156 })
157 .await
158 .expect("query");
159 assert_eq!(rows.len(), 1);
160 }
161
162 #[tokio::test]
163 #[ignore = "requires SPECTRA_TENSORBASE_URL"]
164 async fn tensorbase_events_integration() {
165 let url = std::env::var("SPECTRA_TENSORBASE_URL").expect("SPECTRA_TENSORBASE_URL");
166 let backend = TensorBaseEventsBackend::connect(&url)
167 .await
168 .expect("connect");
169 let ts = Utc::now();
170 backend
171 .append_row("integration_req", &json!({"ok": true}), ts, None)
172 .await
173 .expect("write");
174 let rows = backend
175 .query_rows(EventsQueryFilter {
176 table: "integration_req".into(),
177 ..Default::default()
178 })
179 .await
180 .expect("query");
181 assert!(!rows.is_empty());
182 }
183}