1use std::collections::HashMap;
8use std::future::Future;
9
10use buffa::Message;
11use bytes::Bytes;
12use exoware_sdk::common::kv::v1::Entry;
13use exoware_sdk::log::stream::v1::GetResponse as StreamGetResponse;
14use exoware_sdk::prune_policy::PrunePolicyDocument;
15use exoware_sdk::retention::RetentionPolicy;
16
17use crate::stream::{apply_filter, CompiledMatchers};
18
19pub type QueryExtra = HashMap<String, buffa_types::google::protobuf::Value>;
23
24#[derive(Clone, Debug, Default)]
25pub struct RangeScanBatch {
26 pub rows: Vec<(Bytes, Bytes)>,
28 pub extra: QueryExtra,
30}
31
32pub trait RangeScan: Send {
37 fn next_batch(
41 &mut self,
42 max_items: usize,
43 ) -> impl Future<Output = Result<RangeScanBatch, String>> + Send;
44}
45
46pub trait Sequence: Send + Sync + 'static {
48 fn current_sequence(&self) -> u64;
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56pub enum IngestError {
57 #[error("unavailable: {message}")]
59 Unavailable { message: String },
60
61 #[error("internal: {message}")]
63 Internal { message: String },
64}
65
66pub trait Ingest: Send + Sync + 'static {
68 fn put_batch(
72 &self,
73 kvs: Vec<(Bytes, Bytes)>,
74 ) -> impl Future<Output = Result<u64, IngestError>> + Send;
75}
76
77pub trait Query: Sequence {
79 type RangeScan: RangeScan + 'static;
80
81 fn get(
84 &self,
85 key: Bytes,
86 ) -> impl Future<Output = Result<(Option<Bytes>, QueryExtra), String>> + Send;
87
88 fn range_scan(
92 &self,
93 start: Bytes,
94 end: Bytes,
95 limit: usize,
96 forward: bool,
97 ) -> impl Future<Output = Result<Self::RangeScan, String>> + Send;
98
99 fn get_many(
102 &self,
103 keys: Vec<Bytes>,
104 ) -> impl Future<Output = Result<(Vec<(Bytes, Option<Bytes>)>, QueryExtra), String>> + Send;
105}
106
107pub trait Prune: Send + Sync + 'static {
109 fn apply_prune_policies(
111 &self,
112 document: PrunePolicyDocument,
113 ) -> impl Future<Output = Result<(), String>> + Send;
114}
115
116#[derive(Clone, Debug)]
118pub struct LogBatch {
119 sequence_number: u64,
120 response_bytes: Bytes,
121}
122
123impl LogBatch {
124 pub fn from_response_bytes(sequence_number: u64, response_bytes: impl Into<Bytes>) -> Self {
126 Self {
127 sequence_number,
128 response_bytes: response_bytes.into(),
129 }
130 }
131
132 pub fn from_entries(sequence_number: u64, kvs: Vec<(Bytes, Bytes)>) -> Self {
134 Self::from_response(StreamGetResponse {
135 sequence_number,
136 entries: kvs
137 .into_iter()
138 .map(|(key, value)| Entry {
139 key: key.to_vec(),
140 value,
141 ..Default::default()
142 })
143 .collect(),
144 ..Default::default()
145 })
146 }
147
148 pub fn from_response(response: StreamGetResponse) -> Self {
150 Self {
151 sequence_number: response.sequence_number,
152 response_bytes: Bytes::from(response.encode_to_vec()),
153 }
154 }
155
156 pub fn sequence_number(&self) -> u64 {
158 self.sequence_number
159 }
160
161 pub fn into_response_bytes(self) -> Bytes {
163 self.response_bytes
164 }
165
166 pub fn decode_response(&self) -> Result<StreamGetResponse, String> {
168 StreamGetResponse::decode_from_slice(&self.response_bytes)
169 .map_err(|err| format!("failed to decode sequence log value: {err}"))
170 }
171}
172
173#[derive(Clone, Debug)]
179pub struct FilteredBatch {
180 entries: Vec<Entry>,
181}
182
183impl FilteredBatch {
184 pub fn from_entries(entries: Vec<Entry>) -> Self {
188 Self { entries }
189 }
190
191 pub fn from_kvs(kvs: Vec<(Bytes, Bytes)>) -> Self {
193 Self {
194 entries: kvs
195 .into_iter()
196 .map(|(key, value)| Entry {
197 key: key.to_vec(),
198 value,
199 ..Default::default()
200 })
201 .collect(),
202 }
203 }
204
205 pub fn into_entries(self) -> Vec<Entry> {
207 self.entries
208 }
209}
210
211pub trait Log: Sequence {
213 fn get_batch(
225 &self,
226 sequence_number: u64,
227 ) -> impl Future<Output = Result<Option<LogBatch>, String>> + Send;
228
229 fn get_batch_filtered(
247 &self,
248 sequence_number: u64,
249 matchers: &CompiledMatchers,
250 ) -> impl Future<Output = Result<Option<FilteredBatch>, String>> + Send {
251 async move {
252 let Some(batch) = self.get_batch(sequence_number).await? else {
253 return Ok(None);
254 };
255 let response = batch.decode_response()?;
256 let entries = apply_filter(matchers, response.entries);
257 Ok(Some(FilteredBatch::from_entries(entries)))
258 }
259 }
260
261 fn oldest_retained_batch(&self) -> impl Future<Output = Result<Option<u64>, String>> + Send;
265}
266
267pub trait Retention: Send + Sync + 'static {
269 fn set_retention(
272 &self,
273 policy: Option<RetentionPolicy>,
274 ) -> impl Future<Output = Result<Option<u64>, String>> + Send;
275}
276
277pub trait StoreEngine: Ingest + Query + Prune + Log + Retention {}
279
280impl<T: Ingest + Query + Prune + Log + Retention> StoreEngine for T {}
281
282#[cfg(test)]
283mod tests {
284 use std::future::ready;
285
286 use super::*;
287 use exoware_sdk::kv_codec::Utf8;
288 use exoware_sdk::selector::Selector;
289 use exoware_sdk::stream_filter::StreamFilter;
290
291 #[derive(Clone)]
292 struct TestLog {
293 batch: Option<LogBatch>,
294 }
295
296 impl Sequence for TestLog {
297 fn current_sequence(&self) -> u64 {
298 u64::from(self.batch.is_some())
299 }
300 }
301
302 impl Log for TestLog {
303 fn get_batch(
304 &self,
305 sequence_number: u64,
306 ) -> impl Future<Output = Result<Option<LogBatch>, String>> + Send {
307 ready(Ok((sequence_number == 1)
308 .then(|| self.batch.clone())
309 .flatten()))
310 }
311
312 fn oldest_retained_batch(
313 &self,
314 ) -> impl Future<Output = Result<Option<u64>, String>> + Send {
315 ready(Ok(self.batch.as_ref().map(|_| 1)))
316 }
317 }
318
319 fn matchers() -> CompiledMatchers {
320 crate::stream::compile_matchers(&StreamFilter {
321 selectors: vec![Selector {
322 prefix: Bytes::from_static(&[1]),
323 payload_regex: Utf8::from("^keep$"),
324 }],
325 value_filters: vec![],
326 })
327 .unwrap()
328 }
329
330 #[test]
331 fn default_filtered_batch_preserves_retained_and_missing_states() {
332 let matching = (
333 Bytes::from_static(&[1, b'k', b'e', b'e', b'p']),
334 Bytes::from_static(b"one"),
335 );
336 let excluded = (
337 Bytes::from_static(&[2, b'k', b'e', b'e', b'p']),
338 Bytes::from_static(b"two"),
339 );
340 let log = TestLog {
341 batch: Some(LogBatch::from_entries(1, vec![matching, excluded])),
342 };
343 let filtered = futures::executor::block_on(log.get_batch_filtered(1, &matchers()))
344 .unwrap()
345 .unwrap();
346 let entries = filtered.into_entries();
347 assert_eq!(entries.len(), 1);
348 assert_eq!(entries[0].value.as_ref(), b"one");
349
350 let retained_without_matches = TestLog {
351 batch: Some(LogBatch::from_entries(
352 1,
353 vec![(Bytes::from_static(&[2]), Bytes::from_static(b"value"))],
354 )),
355 };
356 let filtered = futures::executor::block_on(
357 retained_without_matches.get_batch_filtered(1, &matchers()),
358 )
359 .unwrap()
360 .unwrap();
361 assert!(filtered.into_entries().is_empty());
362
363 let missing = TestLog { batch: None };
364 let filtered =
365 futures::executor::block_on(missing.get_batch_filtered(1, &matchers())).unwrap();
366 assert!(filtered.is_none());
367 }
368}