1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
5use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
6use std::sync::Arc;
7use std::time::Duration;
8
9use arrow::array::RecordBatch;
10use arrow::datatypes::SchemaRef;
11use parking_lot::RwLock;
12use tokio::sync::Notify;
13
14use laminar_core::streaming::{self, BackpressureStrategy, SourceConfig, WaitStrategy};
15
16#[derive(Clone, Debug)]
18pub struct ArrowRecord {
19 pub(crate) batch: RecordBatch,
20}
21
22impl laminar_core::streaming::Record for ArrowRecord {
23 fn schema() -> SchemaRef {
24 Arc::new(arrow::datatypes::Schema::empty())
27 }
28
29 fn to_record_batch(&self) -> RecordBatch {
30 self.batch.clone()
31 }
32}
33
34struct SnapshotRing {
39 slots: Box<[parking_lot::Mutex<Option<RecordBatch>>]>,
40 tail: AtomicUsize,
41 capacity: usize,
42}
43
44impl SnapshotRing {
45 fn new(capacity: usize) -> Self {
46 let cap = capacity.max(1);
47 let slots: Vec<_> = (0..cap).map(|_| parking_lot::Mutex::new(None)).collect();
48 Self {
49 slots: slots.into_boxed_slice(),
50 tail: AtomicUsize::new(0),
51 capacity: cap,
52 }
53 }
54
55 fn push(&self, batch: RecordBatch) {
56 let idx = self.tail.fetch_add(1, Ordering::Relaxed) % self.capacity;
58 *self.slots[idx].lock() = Some(batch);
59 }
60
61 fn snapshot(&self) -> Vec<RecordBatch> {
62 let tail = self.tail.load(Ordering::Acquire);
63 let count = tail.min(self.capacity);
64 let start = if tail <= self.capacity {
66 0
67 } else {
68 tail % self.capacity
69 };
70 let mut result = Vec::with_capacity(count);
71 for i in 0..count {
72 let idx = (start + i) % self.capacity;
73 if let Some(batch) = self.slots[idx].lock().as_ref() {
74 result.push(batch.clone());
75 }
76 }
77 result
78 }
79}
80
81pub struct SourceEntry {
83 pub name: String,
85 pub schema: SchemaRef,
87 pub watermark_column: Option<String>,
89 pub max_out_of_orderness: Option<Duration>,
91 pub is_processing_time: std::sync::atomic::AtomicBool,
93 pub(crate) source: streaming::Source<ArrowRecord>,
94 pub(crate) sink: streaming::Sink<ArrowRecord>,
95 buffer: SnapshotRing,
96 data_notify: Arc<Notify>,
98}
99
100impl SourceEntry {
101 pub(crate) fn push_and_buffer(
103 &self,
104 batch: RecordBatch,
105 ) -> Result<(), laminar_core::streaming::StreamingError> {
106 self.source.push_arrow(batch.clone())?;
107 self.buffer.push(batch);
108 self.data_notify.notify_one();
109 Ok(())
110 }
111
112 pub(crate) fn snapshot(&self) -> Vec<RecordBatch> {
113 self.buffer.snapshot()
114 }
115
116 pub(crate) fn data_notify(&self) -> Arc<Notify> {
117 Arc::clone(&self.data_notify)
118 }
119}
120
121pub(crate) struct SinkEntry {
122 pub(crate) input: String,
123}
124
125pub(crate) struct QueryEntry {
126 pub(crate) id: u64,
127 pub(crate) sql: String,
128 pub(crate) active: bool,
129}
130
131pub(crate) struct StreamEntry {
132 pub(crate) name: String,
133 emitted_rows: AtomicU64,
134}
135
136impl StreamEntry {
137 pub(crate) fn record_emitted_rows(&self, rows: u64) {
138 self.emitted_rows.fetch_add(rows, Ordering::Relaxed);
139 }
140
141 pub(crate) fn emitted_rows(&self) -> u64 {
142 self.emitted_rows.load(Ordering::Relaxed)
143 }
144}
145
146pub struct SourceCatalog {
148 sources: RwLock<HashMap<String, Arc<SourceEntry>>>,
149 sinks: RwLock<HashMap<String, SinkEntry>>,
150 streams: RwLock<HashMap<String, Arc<StreamEntry>>>,
151 queries: RwLock<HashMap<u64, QueryEntry>>,
152 next_query_id: AtomicU64,
153 default_buffer_size: usize,
154 default_backpressure: BackpressureStrategy,
155}
156
157impl SourceCatalog {
158 #[must_use]
160 pub fn new(buffer_size: usize, backpressure: BackpressureStrategy) -> Self {
161 Self {
162 sources: RwLock::new(HashMap::new()),
163 sinks: RwLock::new(HashMap::new()),
164 streams: RwLock::new(HashMap::new()),
165 queries: RwLock::new(HashMap::new()),
166 next_query_id: AtomicU64::new(1),
167 default_buffer_size: buffer_size,
168 default_backpressure: backpressure,
169 }
170 }
171
172 #[allow(clippy::too_many_arguments)]
173 pub(crate) fn register_source(
174 &self,
175 name: &str,
176 schema: SchemaRef,
177 watermark_column: Option<String>,
178 max_out_of_orderness: Option<Duration>,
179 buffer_size: Option<usize>,
180 backpressure: Option<BackpressureStrategy>,
181 ) -> Result<Arc<SourceEntry>, crate::DbError> {
182 let mut sources = self.sources.write();
183 if sources.contains_key(name) {
184 return Err(crate::DbError::SourceAlreadyExists(name.to_string()));
185 }
186
187 let buf_size = buffer_size.unwrap_or(self.default_buffer_size);
188 let bp = backpressure.unwrap_or(self.default_backpressure);
189
190 let channel_buf = buf_size.max(1024);
192 let config = SourceConfig {
193 channel: streaming::ChannelConfig {
194 buffer_size: channel_buf,
195 backpressure: bp,
196 wait_strategy: WaitStrategy::SpinYield,
197 track_stats: false,
198 },
199 name: Some(name.to_string()),
200 };
201
202 let (source, sink) = streaming::create_with_config::<ArrowRecord>(config);
203
204 let entry = Arc::new(SourceEntry {
205 name: name.to_string(),
206 schema,
207 watermark_column,
208 max_out_of_orderness,
209 is_processing_time: std::sync::atomic::AtomicBool::new(false),
210 source,
211 sink,
212 buffer: SnapshotRing::new(buf_size),
213 data_notify: Arc::new(Notify::new()),
214 });
215
216 sources.insert(name.to_string(), Arc::clone(&entry));
217 Ok(entry)
218 }
219
220 #[cfg(test)]
221 pub(crate) fn register_source_or_replace(
222 &self,
223 name: &str,
224 schema: SchemaRef,
225 watermark_column: Option<String>,
226 max_out_of_orderness: Option<Duration>,
227 buffer_size: Option<usize>,
228 backpressure: Option<BackpressureStrategy>,
229 ) -> Arc<SourceEntry> {
230 self.sources.write().remove(name);
232 self.register_source(
234 name,
235 schema,
236 watermark_column,
237 max_out_of_orderness,
238 buffer_size,
239 backpressure,
240 )
241 .unwrap()
242 }
243
244 pub fn get_source(&self, name: &str) -> Option<Arc<SourceEntry>> {
246 self.sources.read().get(name).cloned()
247 }
248
249 pub fn drop_source(&self, name: &str) -> bool {
251 self.sources.write().remove(name).is_some()
252 }
253
254 pub(crate) fn register_sink(&self, name: &str, input: &str) -> Result<(), crate::DbError> {
255 let mut sinks = self.sinks.write();
256 if sinks.contains_key(name) {
257 return Err(crate::DbError::SinkAlreadyExists(name.to_string()));
258 }
259 sinks.insert(
260 name.to_string(),
261 SinkEntry {
262 input: input.to_string(),
263 },
264 );
265 Ok(())
266 }
267
268 pub fn drop_sink(&self, name: &str) -> bool {
270 self.sinks.write().remove(name).is_some()
271 }
272
273 pub(crate) fn register_stream(&self, name: &str) -> Result<(), crate::DbError> {
274 let mut streams = self.streams.write();
275 if streams.contains_key(name) {
276 return Err(crate::DbError::StreamAlreadyExists(name.to_string()));
277 }
278
279 streams.insert(
280 name.to_string(),
281 Arc::new(StreamEntry {
282 name: name.to_string(),
283 emitted_rows: AtomicU64::new(0),
284 }),
285 );
286 Ok(())
287 }
288
289 pub(crate) fn get_stream_entry(&self, name: &str) -> Option<Arc<StreamEntry>> {
290 self.streams.read().get(name).cloned()
291 }
292
293 pub fn drop_stream(&self, name: &str) -> bool {
295 self.streams.write().remove(name).is_some()
296 }
297
298 pub fn list_streams(&self) -> Vec<String> {
300 self.streams.read().keys().cloned().collect()
301 }
302
303 pub fn list_sources(&self) -> Vec<String> {
305 self.sources.read().keys().cloned().collect()
306 }
307
308 pub fn list_sinks(&self) -> Vec<String> {
310 self.sinks.read().keys().cloned().collect()
311 }
312
313 pub fn get_sink_input(&self, name: &str) -> Option<String> {
315 self.sinks.read().get(name).map(|e| e.input.clone())
316 }
317
318 pub(crate) fn register_query(&self, sql: &str) -> u64 {
319 let id = self.next_query_id.fetch_add(1, Ordering::Relaxed);
320 let mut queries = self.queries.write();
321 queries.insert(
322 id,
323 QueryEntry {
324 id,
325 sql: sql.to_string(),
326 active: true,
327 },
328 );
329 id
330 }
331
332 pub(crate) fn deactivate_query(&self, id: u64) -> bool {
333 const MAX_INACTIVE_QUERIES: usize = 100;
336 let mut queries = self.queries.write();
337 if let Some(entry) = queries.get_mut(&id) {
338 let was_active = entry.active;
339 entry.active = false;
340 if was_active {
341 let inactive_count = queries.values().filter(|q| !q.active).count();
342 if inactive_count > MAX_INACTIVE_QUERIES {
343 let oldest_inactive_id =
344 queries.values().filter(|q| !q.active).map(|q| q.id).min();
345 if let Some(oldest_id) = oldest_inactive_id {
346 queries.remove(&oldest_id);
347 }
348 }
349 }
350 true
351 } else {
352 false
353 }
354 }
355
356 pub(crate) fn list_queries(&self) -> Vec<(u64, String, bool)> {
357 self.queries
358 .read()
359 .values()
360 .map(|q| (q.id, q.sql.clone(), q.active))
361 .collect()
362 }
363
364 pub fn describe_source(&self, name: &str) -> Option<SchemaRef> {
366 self.sources.read().get(name).map(|e| e.schema.clone())
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use arrow::datatypes::{DataType, Field, Schema};
374
375 fn test_schema() -> SchemaRef {
376 Arc::new(Schema::new(vec![
377 Field::new("id", DataType::Int64, false),
378 Field::new("value", DataType::Float64, false),
379 ]))
380 }
381
382 #[tokio::test]
383 async fn test_register_source() {
384 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
385 let result = catalog.register_source("test", test_schema(), None, None, None, None);
386 assert!(result.is_ok());
387 assert!(catalog.get_source("test").is_some());
388 }
389
390 #[tokio::test]
391 async fn test_register_duplicate_source() {
392 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
393 catalog
394 .register_source("test", test_schema(), None, None, None, None)
395 .unwrap();
396 let result = catalog.register_source("test", test_schema(), None, None, None, None);
397 assert!(matches!(
398 result,
399 Err(crate::DbError::SourceAlreadyExists(_))
400 ));
401 }
402
403 #[tokio::test]
404 async fn test_drop_source() {
405 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
406 catalog
407 .register_source("test", test_schema(), None, None, None, None)
408 .unwrap();
409 assert!(catalog.drop_source("test"));
410 assert!(catalog.get_source("test").is_none());
411 }
412
413 #[tokio::test]
414 async fn test_list_sources() {
415 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
416 catalog
417 .register_source("a", test_schema(), None, None, None, None)
418 .unwrap();
419 catalog
420 .register_source("b", test_schema(), None, None, None, None)
421 .unwrap();
422 let mut names = catalog.list_sources();
423 names.sort();
424 assert_eq!(names, vec!["a", "b"]);
425 }
426
427 #[tokio::test]
428 async fn test_register_sink() {
429 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
430 assert!(catalog.register_sink("output", "events").is_ok());
431 assert_eq!(catalog.list_sinks(), vec!["output"]);
432 }
433
434 #[tokio::test]
435 async fn test_register_query() {
436 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
437 let id = catalog.register_query("SELECT * FROM events");
438 assert_eq!(id, 1);
439 let queries = catalog.list_queries();
440 assert_eq!(queries.len(), 1);
441 assert!(queries[0].2); }
443
444 #[tokio::test]
445 async fn test_deactivate_query() {
446 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
447 let id = catalog.register_query("SELECT * FROM events");
448 catalog.deactivate_query(id);
449 let queries = catalog.list_queries();
450 assert!(!queries[0].2); }
452
453 #[tokio::test]
454 async fn test_deactivate_query_limit() {
455 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
456 let mut ids = Vec::new();
457 for i in 0..105 {
458 let sql = format!("SELECT * FROM events_{i}");
459 ids.push(catalog.register_query(&sql));
460 }
461 for id in &ids {
462 catalog.deactivate_query(*id);
463 }
464 let queries = catalog.list_queries();
465 assert_eq!(queries.len(), 100);
466 let remaining_ids: std::collections::HashSet<u64> = queries.iter().map(|q| q.0).collect();
467 for id in 1..=5 {
468 assert!(
469 !remaining_ids.contains(&id),
470 "Query {id} should have been evicted"
471 );
472 }
473 for id in 6..=105 {
474 assert!(
475 remaining_ids.contains(&id),
476 "Query {id} should be remaining"
477 );
478 }
479 }
480
481 #[tokio::test]
482 async fn test_describe_source() {
483 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
484 let schema = test_schema();
485 catalog
486 .register_source("test", schema.clone(), None, None, None, None)
487 .unwrap();
488 let result = catalog.describe_source("test");
489 assert!(result.is_some());
490 assert_eq!(result.unwrap().fields().len(), 2);
491 }
492
493 #[tokio::test]
494 async fn test_or_replace() {
495 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
496 catalog
497 .register_source("test", test_schema(), None, None, None, None)
498 .unwrap();
499 let entry = catalog.register_source_or_replace(
500 "test",
501 test_schema(),
502 Some("ts".into()),
503 None,
504 None,
505 None,
506 );
507 assert_eq!(entry.watermark_column, Some("ts".to_string()));
508 }
509
510 #[tokio::test]
511 async fn test_push_and_buffer_snapshot() {
512 let catalog = SourceCatalog::new(1024, BackpressureStrategy::Block);
513 let schema = test_schema();
514 let entry = catalog
515 .register_source("test", schema.clone(), None, None, None, None)
516 .unwrap();
517
518 let batch = RecordBatch::try_new(
519 schema,
520 vec![
521 Arc::new(arrow::array::Int64Array::from(vec![1])),
522 Arc::new(arrow::array::Float64Array::from(vec![1.5])),
523 ],
524 )
525 .unwrap();
526
527 entry.push_and_buffer(batch).unwrap();
528 let snap = entry.snapshot();
529 assert_eq!(snap.len(), 1);
530 assert_eq!(snap[0].num_rows(), 1);
531 }
532
533 #[tokio::test]
534 async fn test_buffer_capacity_drops_oldest() {
535 let catalog = SourceCatalog::new(2, BackpressureStrategy::DropOldest);
537 let schema = test_schema();
538 let entry = catalog
539 .register_source("test", schema.clone(), None, None, None, None)
540 .unwrap();
541
542 let values: [(i64, f64); 3] = [(0, 1.0), (1, 2.0), (2, 3.0)];
543 for (id, val) in values {
544 let batch = RecordBatch::try_new(
545 schema.clone(),
546 vec![
547 Arc::new(arrow::array::Int64Array::from(vec![id])),
548 Arc::new(arrow::array::Float64Array::from(vec![val])),
549 ],
550 )
551 .unwrap();
552 entry.push_and_buffer(batch).unwrap();
553 }
554
555 let snap = entry.snapshot();
556 assert_eq!(snap.len(), 2);
558 let col = snap[0]
559 .column(0)
560 .as_any()
561 .downcast_ref::<arrow::array::Int64Array>()
562 .unwrap();
563 assert_eq!(col.value(0), 1); }
565}