1#![allow(clippy::disallowed_types)] use std::collections::{HashMap, VecDeque};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::Arc;
7use std::time::Duration;
8
9use arrow::array::RecordBatch;
10use arrow::datatypes::{Schema, SchemaRef};
11use parking_lot::RwLock;
12use tokio::sync::Notify;
13
14use laminar_core::streaming::{
15 self, BackpressureStrategy, SourceConfig, StreamingError, WaitStrategy,
16};
17
18pub(crate) fn schema_has_reserved_mutation_columns(schema: &Schema) -> bool {
19 schema.fields().iter().any(|field| {
20 ["_op", "__op", laminar_core::changelog::WEIGHT_COLUMN]
21 .iter()
22 .any(|reserved| field.name().eq_ignore_ascii_case(reserved))
23 })
24}
25
26pub(crate) fn validate_source_batch(
27 source_name: &str,
28 expected_schema: &SchemaRef,
29 primary_key: &[String],
30 primary_key_indices: &[usize],
31 batch: &RecordBatch,
32) -> Result<(), StreamingError> {
33 let actual_schema = batch.schema();
34 if !Arc::ptr_eq(&actual_schema, expected_schema)
35 && actual_schema.as_ref() != expected_schema.as_ref()
36 {
37 return Err(StreamingError::SchemaMismatch {
38 expected: expected_schema
39 .fields()
40 .iter()
41 .map(|field| field.name().clone())
42 .collect(),
43 actual: actual_schema
44 .fields()
45 .iter()
46 .map(|field| field.name().clone())
47 .collect(),
48 });
49 }
50 if primary_key.len() != primary_key_indices.len() {
51 return Err(StreamingError::InvalidConfig(format!(
52 "source '{source_name}' primary-key metadata is inconsistent"
53 )));
54 }
55 for (column, &index) in primary_key.iter().zip(primary_key_indices) {
56 let null_count = batch.column(index).null_count();
57 if null_count != 0 {
58 return Err(StreamingError::InvalidConfig(format!(
59 "source '{source_name}' primary-key column '{column}' contains {null_count} null value(s)"
60 )));
61 }
62 }
63 Ok(())
64}
65
66#[derive(Clone, Debug)]
68pub struct ArrowRecord {
69 pub(crate) batch: RecordBatch,
70}
71
72impl laminar_core::streaming::Record for ArrowRecord {
73 fn schema() -> SchemaRef {
74 Arc::new(arrow::datatypes::Schema::empty())
77 }
78
79 fn to_record_batch(&self) -> RecordBatch {
80 self.batch.clone()
81 }
82}
83
84struct SnapshotRing {
86 batches: VecDeque<(RecordBatch, usize)>,
87 bytes: usize,
88 capacity: usize,
89 max_bytes: usize,
90}
91
92impl SnapshotRing {
93 fn new(capacity: usize, max_bytes: usize) -> Self {
94 Self {
95 batches: VecDeque::new(),
96 bytes: 0,
97 capacity: capacity.max(1),
98 max_bytes,
99 }
100 }
101
102 fn push(&mut self, batch: RecordBatch) {
103 let bytes = streaming::retained_arrow_bytes(&batch);
104 debug_assert!(bytes <= self.max_bytes);
107 while self.batches.len() >= self.capacity || self.bytes > self.max_bytes - bytes {
108 if let Some((_, evicted_bytes)) = self.batches.pop_front() {
109 self.bytes -= evicted_bytes;
110 }
111 }
112 self.batches.push_back((batch, bytes));
113 self.bytes += bytes;
114 }
115
116 fn snapshot(&self) -> Vec<RecordBatch> {
117 self.batches
118 .iter()
119 .map(|(batch, _)| batch.clone())
120 .collect()
121 }
122}
123
124pub struct SourceEntry {
126 pub name: String,
128 pub schema: SchemaRef,
130 pub primary_key: Vec<String>,
132 primary_key_indices: Vec<usize>,
133 pub watermark_column: Option<String>,
135 pub max_out_of_orderness: Option<Duration>,
137 pub is_processing_time: std::sync::atomic::AtomicBool,
139 pub(crate) source: streaming::Source<ArrowRecord>,
140 ingress: parking_lot::Mutex<Option<streaming::Sink<ArrowRecord>>>,
143 buffer: parking_lot::Mutex<SnapshotRing>,
144 data_notify: Arc<Notify>,
146}
147
148impl SourceEntry {
149 pub(crate) fn push_and_buffer(
151 &self,
152 batch: RecordBatch,
153 ) -> Result<(), laminar_core::streaming::StreamingError> {
154 validate_source_batch(
155 &self.name,
156 &self.schema,
157 &self.primary_key,
158 &self.primary_key_indices,
159 &batch,
160 )?;
161 let mut buffer = self.buffer.lock();
164 self.source.push_arrow(batch.clone())?;
165 buffer.push(batch);
166 drop(buffer);
167 self.data_notify.notify_one();
168 Ok(())
169 }
170
171 pub(crate) fn snapshot(&self) -> Vec<RecordBatch> {
172 self.buffer.lock().snapshot()
173 }
174
175 pub(crate) fn data_notify(&self) -> Arc<Notify> {
176 Arc::clone(&self.data_notify)
177 }
178
179 pub(crate) fn lease_ingress(self: &Arc<Self>) -> Result<IngressLease, crate::DbError> {
181 let sink = self.ingress.lock().take().ok_or_else(|| {
182 crate::DbError::Pipeline(format!(
183 "source '{}' input queue is already consumed by another pipeline bridge",
184 self.name
185 ))
186 })?;
187 Ok(IngressLease {
188 entry: Arc::clone(self),
189 sink: Some(sink),
190 })
191 }
192
193 pub(crate) fn is_backpressured(&self) -> bool {
194 crate::metrics::is_backpressured(self.source.pending(), self.source.capacity())
195 || crate::metrics::is_backpressured(
196 self.source.queued_arrow_bytes(),
197 self.source.max_queued_bytes(),
198 )
199 }
200}
201
202pub(crate) struct IngressLease {
205 entry: Arc<SourceEntry>,
206 sink: Option<streaming::Sink<ArrowRecord>>,
207}
208
209impl IngressLease {
210 pub(crate) fn poll(&mut self) -> Option<RecordBatch> {
211 self.sink.as_mut().and_then(streaming::Sink::poll)
212 }
213}
214
215impl Drop for IngressLease {
216 fn drop(&mut self) {
217 if let Some(sink) = self.sink.take() {
218 *self.entry.ingress.lock() = Some(sink);
219 }
220 }
221}
222
223pub(crate) struct SinkEntry {
224 pub(crate) input: String,
225}
226
227pub(crate) struct QueryEntry {
228 pub(crate) id: u64,
229 pub(crate) sql: String,
230 pub(crate) active: bool,
231}
232
233pub(crate) struct StreamEntry {
234 pub(crate) name: String,
235 emitted_rows: AtomicU64,
236}
237
238impl StreamEntry {
239 pub(crate) fn record_emitted_rows(&self, rows: u64) {
240 self.emitted_rows.fetch_add(rows, Ordering::Relaxed);
241 }
242
243 pub(crate) fn emitted_rows(&self) -> u64 {
244 self.emitted_rows.load(Ordering::Relaxed)
245 }
246}
247
248pub struct SourceCatalog {
250 sources: RwLock<HashMap<String, Arc<SourceEntry>>>,
251 sinks: RwLock<HashMap<String, SinkEntry>>,
252 streams: RwLock<HashMap<String, Arc<StreamEntry>>>,
253 queries: RwLock<HashMap<u64, QueryEntry>>,
254 next_query_id: AtomicU64,
255 default_buffer_size: usize,
256 default_backpressure: BackpressureStrategy,
257 push_source_max_bytes: usize,
258 #[cfg(feature = "cluster")]
261 topology_planning: bool,
262}
263
264impl SourceCatalog {
265 #[must_use]
267 pub fn new(buffer_size: usize, backpressure: BackpressureStrategy) -> Self {
268 Self {
269 sources: RwLock::new(HashMap::new()),
270 sinks: RwLock::new(HashMap::new()),
271 streams: RwLock::new(HashMap::new()),
272 queries: RwLock::new(HashMap::new()),
273 next_query_id: AtomicU64::new(1),
274 default_buffer_size: buffer_size,
275 default_backpressure: backpressure,
276 push_source_max_bytes: streaming::DEFAULT_SOURCE_MAX_QUEUED_BYTES,
277 #[cfg(feature = "cluster")]
278 topology_planning: false,
279 }
280 }
281
282 pub(crate) fn from_config(config: &crate::LaminarConfig) -> Self {
283 Self {
284 push_source_max_bytes: config.push_source_max_bytes,
285 ..Self::new(config.default_buffer_size, config.default_backpressure)
286 }
287 }
288
289 #[cfg(feature = "cluster")]
290 pub(crate) fn for_topology_planning(config: &crate::LaminarConfig) -> Self {
291 Self {
292 topology_planning: true,
293 ..Self::from_config(config)
294 }
295 }
296
297 #[allow(clippy::too_many_arguments)]
298 pub(crate) fn register_source(
299 &self,
300 name: &str,
301 schema: SchemaRef,
302 primary_key: Vec<String>,
303 watermark_column: Option<String>,
304 max_out_of_orderness: Option<Duration>,
305 buffer_size: Option<usize>,
306 backpressure: Option<BackpressureStrategy>,
307 ) -> Result<Arc<SourceEntry>, crate::DbError> {
308 let mut sources = self.sources.write();
309 if sources.contains_key(name) {
310 return Err(crate::DbError::SourceAlreadyExists(name.to_string()));
311 }
312
313 let mut primary_key_indices = Vec::with_capacity(primary_key.len());
314 for column in &primary_key {
315 let index = schema.index_of(column).map_err(|_| {
316 crate::DbError::InvalidOperation(format!(
317 "source '{name}' primary-key column '{column}' is absent from its schema"
318 ))
319 })?;
320 if primary_key_indices.contains(&index) {
321 return Err(crate::DbError::InvalidOperation(format!(
322 "source '{name}' primary key repeats column '{column}'"
323 )));
324 }
325 if schema.field(index).is_nullable() {
326 return Err(crate::DbError::InvalidOperation(format!(
327 "source '{name}' primary-key column '{column}' must be non-nullable"
328 )));
329 }
330 primary_key_indices.push(index);
331 }
332
333 let buf_size = buffer_size.unwrap_or(self.default_buffer_size);
334 #[cfg(feature = "cluster")]
335 let buf_size = if self.topology_planning { 1 } else { buf_size };
336 let bp = backpressure.unwrap_or(self.default_backpressure);
337
338 let channel_buf = buf_size.max(1024);
340 let config = SourceConfig {
341 channel: streaming::ChannelConfig {
342 buffer_size: channel_buf,
343 backpressure: bp,
344 wait_strategy: WaitStrategy::SpinYield,
345 track_stats: false,
346 },
347 name: Some(name.to_string()),
348 max_queued_bytes: self.push_source_max_bytes,
349 };
350
351 #[cfg(feature = "cluster")]
352 let (source, ingress) = if self.topology_planning {
353 (
354 streaming::create_for_schema_planning::<ArrowRecord>(config),
355 None,
356 )
357 } else {
358 let (source, sink) = streaming::create_with_config::<ArrowRecord>(config);
359 (source, Some(sink))
360 };
361 #[cfg(not(feature = "cluster"))]
362 let (source, ingress) = {
363 let (source, sink) = streaming::create_with_config::<ArrowRecord>(config);
364 (source, Some(sink))
365 };
366
367 let entry = Arc::new(SourceEntry {
368 name: name.to_string(),
369 schema,
370 primary_key,
371 primary_key_indices,
372 watermark_column,
373 max_out_of_orderness,
374 is_processing_time: std::sync::atomic::AtomicBool::new(false),
375 source,
376 ingress: parking_lot::Mutex::new(ingress),
377 buffer: parking_lot::Mutex::new(SnapshotRing::new(
378 buf_size,
379 self.push_source_max_bytes,
380 )),
381 data_notify: Arc::new(Notify::new()),
382 });
383
384 sources.insert(name.to_string(), Arc::clone(&entry));
385 Ok(entry)
386 }
387
388 #[cfg(test)]
389 pub(crate) fn register_source_or_replace(
390 &self,
391 name: &str,
392 schema: SchemaRef,
393 primary_key: Vec<String>,
394 watermark_column: Option<String>,
395 max_out_of_orderness: Option<Duration>,
396 buffer_size: Option<usize>,
397 backpressure: Option<BackpressureStrategy>,
398 ) -> Arc<SourceEntry> {
399 self.sources.write().remove(name);
401 self.register_source(
403 name,
404 schema,
405 primary_key,
406 watermark_column,
407 max_out_of_orderness,
408 buffer_size,
409 backpressure,
410 )
411 .unwrap()
412 }
413
414 pub fn get_source(&self, name: &str) -> Option<Arc<SourceEntry>> {
416 self.sources.read().get(name).cloned()
417 }
418
419 pub fn drop_source(&self, name: &str) -> bool {
421 self.sources.write().remove(name).is_some()
422 }
423
424 pub(crate) fn register_sink(&self, name: &str, input: &str) -> Result<(), crate::DbError> {
425 let mut sinks = self.sinks.write();
426 if sinks.contains_key(name) {
427 return Err(crate::DbError::SinkAlreadyExists(name.to_string()));
428 }
429 sinks.insert(
430 name.to_string(),
431 SinkEntry {
432 input: input.to_string(),
433 },
434 );
435 Ok(())
436 }
437
438 pub fn drop_sink(&self, name: &str) -> bool {
440 self.sinks.write().remove(name).is_some()
441 }
442
443 pub(crate) fn register_stream(&self, name: &str) -> Result<(), crate::DbError> {
444 let mut streams = self.streams.write();
445 if streams.contains_key(name) {
446 return Err(crate::DbError::StreamAlreadyExists(name.to_string()));
447 }
448
449 streams.insert(
450 name.to_string(),
451 Arc::new(StreamEntry {
452 name: name.to_string(),
453 emitted_rows: AtomicU64::new(0),
454 }),
455 );
456 Ok(())
457 }
458
459 pub(crate) fn get_stream_entry(&self, name: &str) -> Option<Arc<StreamEntry>> {
460 self.streams.read().get(name).cloned()
461 }
462
463 pub fn drop_stream(&self, name: &str) -> bool {
465 self.streams.write().remove(name).is_some()
466 }
467
468 pub fn list_streams(&self) -> Vec<String> {
470 self.streams.read().keys().cloned().collect()
471 }
472
473 pub fn list_sources(&self) -> Vec<String> {
475 self.sources.read().keys().cloned().collect()
476 }
477
478 pub fn list_sinks(&self) -> Vec<String> {
480 self.sinks.read().keys().cloned().collect()
481 }
482
483 pub fn get_sink_input(&self, name: &str) -> Option<String> {
485 self.sinks.read().get(name).map(|e| e.input.clone())
486 }
487
488 pub(crate) fn register_query(&self, sql: &str) -> u64 {
489 let id = self.next_query_id.fetch_add(1, Ordering::Relaxed);
490 let mut queries = self.queries.write();
491 queries.insert(
492 id,
493 QueryEntry {
494 id,
495 sql: sql.to_string(),
496 active: true,
497 },
498 );
499 id
500 }
501
502 pub(crate) fn deactivate_query(&self, id: u64) -> bool {
503 const MAX_INACTIVE_QUERIES: usize = 100;
506 let mut queries = self.queries.write();
507 if let Some(entry) = queries.get_mut(&id) {
508 let was_active = entry.active;
509 entry.active = false;
510 if was_active {
511 let inactive_count = queries.values().filter(|q| !q.active).count();
512 if inactive_count > MAX_INACTIVE_QUERIES {
513 let oldest_inactive_id =
514 queries.values().filter(|q| !q.active).map(|q| q.id).min();
515 if let Some(oldest_id) = oldest_inactive_id {
516 queries.remove(&oldest_id);
517 }
518 }
519 }
520 true
521 } else {
522 false
523 }
524 }
525
526 pub(crate) fn list_queries(&self) -> Vec<(u64, String, bool)> {
527 self.queries
528 .read()
529 .values()
530 .map(|q| (q.id, q.sql.clone(), q.active))
531 .collect()
532 }
533
534 pub fn describe_source(&self, name: &str) -> Option<SchemaRef> {
536 self.sources.read().get(name).map(|e| e.schema.clone())
537 }
538}
539
540#[cfg(test)]
541mod tests;
542
543#[cfg(test)]
544mod admission_tests;