laminar_db/process_function/
registration.rs1use std::sync::Arc;
2
3use datafusion::datasource::empty::EmptyTable;
4use laminar_connectors::connector::DeliveryGuarantee;
5use laminar_core::catalog::CatalogObjectKind;
6
7use super::{
8 NativeProcessFunction, ProcessFunctionDescriptor, ProcessFunctionInfo,
9 ProcessFunctionRegistration, ProcessHandler, ProcessRuntime,
10};
11use crate::db::{exact_table_reference, DbState, LaminarDB};
12use crate::error::DbError;
13
14impl LaminarDB {
15 #[must_use]
17 pub fn process_functions(&self) -> Vec<ProcessFunctionInfo> {
18 let manager = self.connector_manager.lock();
19 let mut functions = manager
20 .process_functions()
21 .values()
22 .map(|registration| ProcessFunctionInfo {
23 output_name: registration.output_name.clone(),
24 source_name: registration.source_name.clone(),
25 descriptor: registration.descriptor.clone(),
26 })
27 .collect::<Vec<_>>();
28 functions.sort_unstable_by(|left, right| left.output_name.cmp(&right.output_name));
29 functions
30 }
31
32 pub async fn register_native_process_function(
50 &self,
51 output_name: &str,
52 source_name: &str,
53 descriptor: ProcessFunctionDescriptor,
54 handler: Arc<dyn NativeProcessFunction>,
55 ) -> Result<(), DbError> {
56 if descriptor.runtime != ProcessRuntime::NativeRust {
57 return Err(DbError::Unsupported(
58 "native registration requires the trusted native Rust runtime".into(),
59 ));
60 }
61 self.register_process_function(
62 output_name,
63 source_name,
64 descriptor,
65 ProcessHandler::Native(handler),
66 )
67 .await
68 }
69
70 #[cfg(feature = "process-remote")]
80 pub async fn register_remote_process_function(
81 &self,
82 output_name: &str,
83 source_name: &str,
84 descriptor: ProcessFunctionDescriptor,
85 client: Arc<super::remote::RemoteProcessClient>,
86 ) -> Result<(), DbError> {
87 if descriptor.runtime == ProcessRuntime::NativeRust {
88 return Err(DbError::InvalidOperation(
89 "remote registration requires a remote runtime".into(),
90 ));
91 }
92 if descriptor.to_manifest_json()? != client.descriptor().to_manifest_json()? {
93 return Err(DbError::InvalidOperation(
94 "connected process worker descriptor differs from registration".into(),
95 ));
96 }
97 self.register_process_function(
98 output_name,
99 source_name,
100 descriptor,
101 ProcessHandler::Remote(client),
102 )
103 .await
104 }
105
106 async fn register_process_function(
107 &self,
108 output_name: &str,
109 source_name: &str,
110 descriptor: ProcessFunctionDescriptor,
111 handler: ProcessHandler,
112 ) -> Result<(), DbError> {
113 let _topology = self.topology_ddl_lock.write().await;
114 self.ensure_topology_ddl_allowed("REGISTER PROCESS FUNCTION")?;
115 if DbState::load(&self.state) != DbState::Created {
116 return Err(DbError::Unsupported(
117 "process functions require registration before pipeline startup".into(),
118 ));
119 }
120 if self.config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce {
121 return Err(DbError::Unsupported(
122 "process functions do not support exactly-once delivery".into(),
123 ));
124 }
125 if self.config.delivery_guarantee == DeliveryGuarantee::AtLeastOnce
126 && !handler.supports_replay(&descriptor)
127 {
128 return Err(DbError::Unsupported(
129 "at-least-once Python process functions require immutable dependency binding from a supervised replay-safe worker"
130 .into(),
131 ));
132 }
133 if !valid_name(output_name) || !valid_name(source_name) || output_name == source_name {
134 return Err(DbError::InvalidOperation(
135 "process source and output require distinct lowercase SQL identifiers".into(),
136 ));
137 }
138 descriptor.to_manifest_json()?;
139 let registration = ProcessFunctionRegistration {
140 output_name: output_name.into(),
141 source_name: source_name.into(),
142 descriptor,
143 handler,
144 };
145 if self.is_cluster_runtime() {
146 if self.config.delivery_guarantee != DeliveryGuarantee::AtLeastOnce {
147 return Err(DbError::Unsupported(
148 "cluster process functions require at-least-once delivery".into(),
149 ));
150 }
151 let mut manager = self.connector_manager.lock();
152 if manager.process_functions().contains_key(output_name) {
153 return Err(DbError::InvalidOperation(
154 "process binding already exists".into(),
155 ));
156 }
157 manager.register_process_function(registration);
158 return Ok(());
159 }
160 self.install_process_function(®istration)
161 }
162
163 pub(crate) fn install_process_function(
164 &self,
165 registration: &ProcessFunctionRegistration,
166 ) -> Result<(), DbError> {
167 let ProcessFunctionRegistration {
168 output_name,
169 source_name,
170 descriptor,
171 ..
172 } = registration;
173 let source = self.catalog.get_source(source_name).ok_or_else(|| {
174 DbError::InvalidOperation(format!("process source '{source_name}' does not exist"))
175 })?;
176 self.validate_process_source_order(
177 output_name,
178 source_name,
179 self.connector_manager.lock().sources(),
180 )?;
181 if source.schema.as_ref() != descriptor.input_schema.as_ref() {
182 return Err(DbError::InvalidOperation(
183 "process descriptor input schema differs from its source".into(),
184 ));
185 }
186 if source.watermark_column.as_deref() != Some(descriptor.event_time_column.as_str())
187 || source
188 .is_processing_time
189 .load(std::sync::atomic::Ordering::Acquire)
190 || !source.primary_key.is_empty()
191 {
192 return Err(DbError::Unsupported(
193 "process source requires an append-only event-time watermark on the declared column"
194 .into(),
195 ));
196 }
197 let reservation = self
198 .reserve_catalog_name(output_name, CatalogObjectKind::Stream, false)?
199 .ok_or_else(|| DbError::InvalidOperation("process output already exists".into()))?;
200 self.catalog.register_stream(output_name)?;
201 self.connector_manager
202 .lock()
203 .register_process_function(registration.clone());
204 self.ctx
205 .register_table(
206 exact_table_reference(output_name),
207 Arc::new(EmptyTable::new(Arc::clone(&descriptor.output_schema))),
208 )
209 .map_err(|error| {
210 DbError::Pipeline(format!(
211 "could not register process output '{output_name}' for SQL planning: {error}"
212 ))
213 })?;
214 self.stream_schemas
215 .write()
216 .insert(output_name.clone(), Arc::clone(&descriptor.output_schema));
217 reservation.commit();
218 Ok(())
219 }
220}
221
222fn valid_name(name: &str) -> bool {
223 if name.len() > 128 {
224 return false;
225 }
226 let mut bytes = name.bytes();
227 matches!(bytes.next(), Some(b'a'..=b'z' | b'_'))
228 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
229}