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::{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 {
89 slots: Box<[parking_lot::Mutex<Option<RecordBatch>>]>,
90 tail: AtomicUsize,
91 capacity: usize,
92}
93
94impl SnapshotRing {
95 fn new(capacity: usize) -> Self {
96 let cap = capacity.max(1);
97 let slots: Vec<_> = (0..cap).map(|_| parking_lot::Mutex::new(None)).collect();
98 Self {
99 slots: slots.into_boxed_slice(),
100 tail: AtomicUsize::new(0),
101 capacity: cap,
102 }
103 }
104
105 fn push(&self, batch: RecordBatch) {
106 let idx = self.tail.fetch_add(1, Ordering::Relaxed) % self.capacity;
108 *self.slots[idx].lock() = Some(batch);
109 }
110
111 fn snapshot(&self) -> Vec<RecordBatch> {
112 let tail = self.tail.load(Ordering::Acquire);
113 let count = tail.min(self.capacity);
114 let start = if tail <= self.capacity {
116 0
117 } else {
118 tail % self.capacity
119 };
120 let mut result = Vec::with_capacity(count);
121 for i in 0..count {
122 let idx = (start + i) % self.capacity;
123 if let Some(batch) = self.slots[idx].lock().as_ref() {
124 result.push(batch.clone());
125 }
126 }
127 result
128 }
129}
130
131pub struct SourceEntry {
133 pub name: String,
135 pub schema: SchemaRef,
137 pub primary_key: Vec<String>,
139 primary_key_indices: Vec<usize>,
140 pub watermark_column: Option<String>,
142 pub max_out_of_orderness: Option<Duration>,
144 pub is_processing_time: std::sync::atomic::AtomicBool,
146 pub(crate) source: streaming::Source<ArrowRecord>,
147 pub(crate) sink: streaming::Sink<ArrowRecord>,
148 buffer: SnapshotRing,
149 data_notify: Arc<Notify>,
151}
152
153impl SourceEntry {
154 pub(crate) fn push_and_buffer(
156 &self,
157 batch: RecordBatch,
158 ) -> Result<(), laminar_core::streaming::StreamingError> {
159 validate_source_batch(
160 &self.name,
161 &self.schema,
162 &self.primary_key,
163 &self.primary_key_indices,
164 &batch,
165 )?;
166 self.source.push_arrow(batch.clone())?;
167 self.buffer.push(batch);
168 self.data_notify.notify_one();
169 Ok(())
170 }
171
172 pub(crate) fn snapshot(&self) -> Vec<RecordBatch> {
173 self.buffer.snapshot()
174 }
175
176 pub(crate) fn data_notify(&self) -> Arc<Notify> {
177 Arc::clone(&self.data_notify)
178 }
179}
180
181pub(crate) struct SinkEntry {
182 pub(crate) input: String,
183}
184
185pub(crate) struct QueryEntry {
186 pub(crate) id: u64,
187 pub(crate) sql: String,
188 pub(crate) active: bool,
189}
190
191pub(crate) struct StreamEntry {
192 pub(crate) name: String,
193 emitted_rows: AtomicU64,
194}
195
196impl StreamEntry {
197 pub(crate) fn record_emitted_rows(&self, rows: u64) {
198 self.emitted_rows.fetch_add(rows, Ordering::Relaxed);
199 }
200
201 pub(crate) fn emitted_rows(&self) -> u64 {
202 self.emitted_rows.load(Ordering::Relaxed)
203 }
204}
205
206pub struct SourceCatalog {
208 sources: RwLock<HashMap<String, Arc<SourceEntry>>>,
209 sinks: RwLock<HashMap<String, SinkEntry>>,
210 streams: RwLock<HashMap<String, Arc<StreamEntry>>>,
211 queries: RwLock<HashMap<u64, QueryEntry>>,
212 next_query_id: AtomicU64,
213 default_buffer_size: usize,
214 default_backpressure: BackpressureStrategy,
215}
216
217impl SourceCatalog {
218 #[must_use]
220 pub fn new(buffer_size: usize, backpressure: BackpressureStrategy) -> Self {
221 Self {
222 sources: RwLock::new(HashMap::new()),
223 sinks: RwLock::new(HashMap::new()),
224 streams: RwLock::new(HashMap::new()),
225 queries: RwLock::new(HashMap::new()),
226 next_query_id: AtomicU64::new(1),
227 default_buffer_size: buffer_size,
228 default_backpressure: backpressure,
229 }
230 }
231
232 #[allow(clippy::too_many_arguments)]
233 pub(crate) fn register_source(
234 &self,
235 name: &str,
236 schema: SchemaRef,
237 primary_key: Vec<String>,
238 watermark_column: Option<String>,
239 max_out_of_orderness: Option<Duration>,
240 buffer_size: Option<usize>,
241 backpressure: Option<BackpressureStrategy>,
242 ) -> Result<Arc<SourceEntry>, crate::DbError> {
243 let mut sources = self.sources.write();
244 if sources.contains_key(name) {
245 return Err(crate::DbError::SourceAlreadyExists(name.to_string()));
246 }
247
248 let mut primary_key_indices = Vec::with_capacity(primary_key.len());
249 for column in &primary_key {
250 let index = schema.index_of(column).map_err(|_| {
251 crate::DbError::InvalidOperation(format!(
252 "source '{name}' primary-key column '{column}' is absent from its schema"
253 ))
254 })?;
255 if primary_key_indices.contains(&index) {
256 return Err(crate::DbError::InvalidOperation(format!(
257 "source '{name}' primary key repeats column '{column}'"
258 )));
259 }
260 if schema.field(index).is_nullable() {
261 return Err(crate::DbError::InvalidOperation(format!(
262 "source '{name}' primary-key column '{column}' must be non-nullable"
263 )));
264 }
265 primary_key_indices.push(index);
266 }
267
268 let buf_size = buffer_size.unwrap_or(self.default_buffer_size);
269 let bp = backpressure.unwrap_or(self.default_backpressure);
270
271 let channel_buf = buf_size.max(1024);
273 let config = SourceConfig {
274 channel: streaming::ChannelConfig {
275 buffer_size: channel_buf,
276 backpressure: bp,
277 wait_strategy: WaitStrategy::SpinYield,
278 track_stats: false,
279 },
280 name: Some(name.to_string()),
281 };
282
283 let (source, sink) = streaming::create_with_config::<ArrowRecord>(config);
284
285 let entry = Arc::new(SourceEntry {
286 name: name.to_string(),
287 schema,
288 primary_key,
289 primary_key_indices,
290 watermark_column,
291 max_out_of_orderness,
292 is_processing_time: std::sync::atomic::AtomicBool::new(false),
293 source,
294 sink,
295 buffer: SnapshotRing::new(buf_size),
296 data_notify: Arc::new(Notify::new()),
297 });
298
299 sources.insert(name.to_string(), Arc::clone(&entry));
300 Ok(entry)
301 }
302
303 #[cfg(test)]
304 pub(crate) fn register_source_or_replace(
305 &self,
306 name: &str,
307 schema: SchemaRef,
308 primary_key: Vec<String>,
309 watermark_column: Option<String>,
310 max_out_of_orderness: Option<Duration>,
311 buffer_size: Option<usize>,
312 backpressure: Option<BackpressureStrategy>,
313 ) -> Arc<SourceEntry> {
314 self.sources.write().remove(name);
316 self.register_source(
318 name,
319 schema,
320 primary_key,
321 watermark_column,
322 max_out_of_orderness,
323 buffer_size,
324 backpressure,
325 )
326 .unwrap()
327 }
328
329 pub fn get_source(&self, name: &str) -> Option<Arc<SourceEntry>> {
331 self.sources.read().get(name).cloned()
332 }
333
334 pub fn drop_source(&self, name: &str) -> bool {
336 self.sources.write().remove(name).is_some()
337 }
338
339 pub(crate) fn register_sink(&self, name: &str, input: &str) -> Result<(), crate::DbError> {
340 let mut sinks = self.sinks.write();
341 if sinks.contains_key(name) {
342 return Err(crate::DbError::SinkAlreadyExists(name.to_string()));
343 }
344 sinks.insert(
345 name.to_string(),
346 SinkEntry {
347 input: input.to_string(),
348 },
349 );
350 Ok(())
351 }
352
353 pub fn drop_sink(&self, name: &str) -> bool {
355 self.sinks.write().remove(name).is_some()
356 }
357
358 pub(crate) fn register_stream(&self, name: &str) -> Result<(), crate::DbError> {
359 let mut streams = self.streams.write();
360 if streams.contains_key(name) {
361 return Err(crate::DbError::StreamAlreadyExists(name.to_string()));
362 }
363
364 streams.insert(
365 name.to_string(),
366 Arc::new(StreamEntry {
367 name: name.to_string(),
368 emitted_rows: AtomicU64::new(0),
369 }),
370 );
371 Ok(())
372 }
373
374 pub(crate) fn get_stream_entry(&self, name: &str) -> Option<Arc<StreamEntry>> {
375 self.streams.read().get(name).cloned()
376 }
377
378 pub fn drop_stream(&self, name: &str) -> bool {
380 self.streams.write().remove(name).is_some()
381 }
382
383 pub fn list_streams(&self) -> Vec<String> {
385 self.streams.read().keys().cloned().collect()
386 }
387
388 pub fn list_sources(&self) -> Vec<String> {
390 self.sources.read().keys().cloned().collect()
391 }
392
393 pub fn list_sinks(&self) -> Vec<String> {
395 self.sinks.read().keys().cloned().collect()
396 }
397
398 pub fn get_sink_input(&self, name: &str) -> Option<String> {
400 self.sinks.read().get(name).map(|e| e.input.clone())
401 }
402
403 pub(crate) fn register_query(&self, sql: &str) -> u64 {
404 let id = self.next_query_id.fetch_add(1, Ordering::Relaxed);
405 let mut queries = self.queries.write();
406 queries.insert(
407 id,
408 QueryEntry {
409 id,
410 sql: sql.to_string(),
411 active: true,
412 },
413 );
414 id
415 }
416
417 pub(crate) fn deactivate_query(&self, id: u64) -> bool {
418 const MAX_INACTIVE_QUERIES: usize = 100;
421 let mut queries = self.queries.write();
422 if let Some(entry) = queries.get_mut(&id) {
423 let was_active = entry.active;
424 entry.active = false;
425 if was_active {
426 let inactive_count = queries.values().filter(|q| !q.active).count();
427 if inactive_count > MAX_INACTIVE_QUERIES {
428 let oldest_inactive_id =
429 queries.values().filter(|q| !q.active).map(|q| q.id).min();
430 if let Some(oldest_id) = oldest_inactive_id {
431 queries.remove(&oldest_id);
432 }
433 }
434 }
435 true
436 } else {
437 false
438 }
439 }
440
441 pub(crate) fn list_queries(&self) -> Vec<(u64, String, bool)> {
442 self.queries
443 .read()
444 .values()
445 .map(|q| (q.id, q.sql.clone(), q.active))
446 .collect()
447 }
448
449 pub fn describe_source(&self, name: &str) -> Option<SchemaRef> {
451 self.sources.read().get(name).map(|e| e.schema.clone())
452 }
453}
454
455#[cfg(test)]
456mod tests;