1#[cfg(feature = "delta-lake")]
5mod commit_descriptor;
6
7pub mod delta;
9pub mod delta_config;
10#[cfg(feature = "delta-lake")]
11pub mod delta_io;
12#[cfg(feature = "delta-lake")]
13pub mod delta_lookup;
14pub mod delta_metrics;
15#[cfg(feature = "delta-lake")]
16pub mod delta_reference;
17pub mod delta_source;
18pub mod delta_source_config;
19#[cfg(feature = "delta-lake")]
20pub mod delta_table_provider;
21#[cfg(feature = "delta-lake-unity")]
22pub(crate) mod unity_catalog;
23
24pub mod iceberg;
26pub mod iceberg_config;
27#[cfg(feature = "iceberg-core")]
28pub mod iceberg_io;
29#[cfg(feature = "iceberg-core")]
30pub mod iceberg_lookup;
31#[cfg(feature = "iceberg-core")]
32pub mod iceberg_reference;
33#[cfg(feature = "iceberg-core")]
34mod iceberg_scan;
35pub mod iceberg_source;
36
37pub mod metrics;
39#[cfg(any(test, feature = "delta-lake", feature = "iceberg-core"))]
40mod snapshot_schema;
41
42pub use delta::DeltaLakeSink;
44pub use delta_config::{DeltaCatalogType, DeltaLakeSinkConfig, DeltaWriteMode};
45#[cfg(feature = "delta-lake")]
46pub use delta_lookup::{DeltaLookupSource, DeltaLookupSourceConfig};
47pub use delta_metrics::DeltaLakeSinkMetrics;
48#[cfg(feature = "delta-lake")]
49pub use delta_reference::DeltaReferenceTableSource;
50pub use delta_source::DeltaSource;
51pub use delta_source_config::DeltaSourceConfig;
52pub use metrics::LakehouseSinkMetrics;
53
54pub use iceberg::IcebergSink;
56use iceberg_config::{
57 sink_config_keys as iceberg_sink_config_keys, source_config_keys as iceberg_source_config_keys,
58};
59pub use iceberg_config::{
60 IcebergCatalogAuthType, IcebergCatalogConfig, IcebergCatalogType, IcebergNullOrder,
61 IcebergPartitionField, IcebergReadBootstrap, IcebergReadMode, IcebergSchemaEvolutionMode,
62 IcebergSinkConfig, IcebergSortDirection, IcebergSortField, IcebergSourceConfig,
63 IcebergStorageConfig, IcebergStorageEncryption, IcebergStorageType, IcebergTransform,
64 IcebergWriteDistributionMode, IcebergWriteMode,
65};
66#[cfg(feature = "iceberg-core")]
67pub use iceberg_lookup::{IcebergLookupSource, IcebergLookupSourceConfig};
68#[cfg(feature = "iceberg-core")]
69pub use iceberg_reference::IcebergReferenceTableSource;
70pub use iceberg_source::IcebergSource;
71
72use std::sync::Arc;
73
74use crate::config::{ConfigKeySpec, ConnectorInfo};
75use crate::registry::ConnectorRegistry;
76
77pub fn register_delta_lake_sink(
83 registry: &ConnectorRegistry,
84) -> Result<(), crate::error::ConnectorError> {
85 let info = ConnectorInfo {
86 name: "delta-lake".to_string(),
87 display_name: "Delta Lake Sink".to_string(),
88 version: env!("CARGO_PKG_VERSION").to_string(),
89 is_source: false,
90 is_sink: true,
91 config_keys: delta_lake_config_keys(),
92 };
93
94 registry.register_sink(
95 "delta-lake",
96 info,
97 Arc::new(|config, registry: Option<&Arc<prometheus::Registry>>| {
98 Ok(Box::new(DeltaLakeSink::new(
99 DeltaLakeSinkConfig::from_config(config)?,
100 registry.map(Arc::as_ref),
101 )))
102 }),
103 )
104}
105
106pub fn register_delta_lake_source(
112 registry: &ConnectorRegistry,
113) -> Result<(), crate::error::ConnectorError> {
114 let info = ConnectorInfo {
115 name: "delta-lake".to_string(),
116 display_name: "Delta Lake Source".to_string(),
117 version: env!("CARGO_PKG_VERSION").to_string(),
118 is_source: true,
119 is_sink: false,
120 config_keys: delta_lake_source_config_keys(),
121 };
122
123 registry.register_source(
124 "delta-lake",
125 info.clone(),
126 Arc::new(|registry: Option<&Arc<prometheus::Registry>>| {
127 Ok(Box::new(DeltaSource::new(
128 DeltaSourceConfig::default(),
129 registry.map(Arc::as_ref),
130 )))
131 }),
132 )?;
133
134 #[cfg(feature = "delta-lake")]
136 registry.register_table_source(
137 "delta-lake",
138 info.clone(),
139 Arc::new(|config, declared_schema| {
140 Ok(Box::new(DeltaReferenceTableSource::from_connector_config(
141 config,
142 declared_schema,
143 )?))
144 }),
145 )?;
146
147 #[cfg(feature = "delta-lake")]
149 registry.register_lookup_source("delta-lake", info, Arc::new(DeltaLookupFactory))?;
150
151 Ok(())
152}
153
154#[cfg(feature = "delta-lake")]
155struct DeltaLookupFactory;
156
157#[cfg(feature = "delta-lake")]
158#[async_trait::async_trait]
159impl crate::registry::LookupSourceFactory for DeltaLookupFactory {
160 async fn build(
161 &self,
162 config: crate::config::ConnectorConfig,
163 _declared_schema: Option<arrow_schema::SchemaRef>,
164 ) -> Result<Arc<dyn laminar_core::lookup::source::LookupSourceDyn>, crate::error::ConnectorError>
165 {
166 use crate::lakehouse::delta_source_config::DeltaSourceConfig;
167 let pk_columns: Vec<String> = config
168 .get("_primary_key_columns")
169 .unwrap_or("")
170 .split(',')
171 .map(|s| s.trim().to_string())
172 .filter(|s| !s.is_empty())
173 .collect();
174
175 if pk_columns.is_empty() {
176 return Err(crate::error::ConnectorError::ConfigurationError(
177 "delta-lake lookup source requires primary key columns".into(),
178 ));
179 }
180
181 let src_config = DeltaSourceConfig::from_config(&config)?;
182
183 let (resolved_path, resolved_opts) = crate::lakehouse::delta_io::resolve_catalog_options(
184 &src_config.catalog_type,
185 src_config.catalog_database.as_deref(),
186 src_config.catalog_name.as_deref(),
187 src_config.catalog_schema.as_deref(),
188 &src_config.table_path,
189 &src_config.storage_options,
190 )
191 .await?;
192
193 let lookup_config = DeltaLookupSourceConfig {
194 table_path: resolved_path,
195 storage_options: resolved_opts,
196 primary_key_columns: pk_columns,
197 table_name: "delta_lookup".to_string(),
198 };
199
200 let source = DeltaLookupSource::open(lookup_config).await?;
202
203 Ok(Arc::new(source) as Arc<dyn laminar_core::lookup::source::LookupSourceDyn>)
204 }
205}
206
207pub fn register_iceberg_sink(
213 registry: &ConnectorRegistry,
214) -> Result<(), crate::error::ConnectorError> {
215 let info = ConnectorInfo {
216 name: "iceberg".to_string(),
217 display_name: "Apache Iceberg Sink".to_string(),
218 version: env!("CARGO_PKG_VERSION").to_string(),
219 is_source: false,
220 is_sink: true,
221 config_keys: iceberg_sink_config_keys(),
222 };
223
224 registry.register_sink(
225 "iceberg",
226 info,
227 Arc::new(|config, registry: Option<&Arc<prometheus::Registry>>| {
228 Ok(Box::new(IcebergSink::new(
229 IcebergSinkConfig::from_config(config)?,
230 registry.map(Arc::as_ref),
231 )))
232 }),
233 )
234}
235
236pub fn register_iceberg_source(
242 registry: &ConnectorRegistry,
243) -> Result<(), crate::error::ConnectorError> {
244 let info = ConnectorInfo {
245 name: "iceberg".to_string(),
246 display_name: "Apache Iceberg Source".to_string(),
247 version: env!("CARGO_PKG_VERSION").to_string(),
248 is_source: true,
249 is_sink: false,
250 config_keys: iceberg_source_config_keys(),
251 };
252
253 registry.register_source(
254 "iceberg",
255 info.clone(),
256 Arc::new(|registry: Option<&Arc<prometheus::Registry>>| {
257 let mut config = crate::config::ConnectorConfig::new("iceberg");
258 config.set("catalog.uri", "http://localhost:8181");
259 config.set("catalog.warehouse", "s3://default/wh");
260 config.set("namespace", "default");
261 config.set("table.name", "default");
262 Ok(Box::new(IcebergSource::new(
263 IcebergSourceConfig::from_config(&config)?,
264 registry.map(Arc::as_ref),
265 )))
266 }),
267 )?;
268
269 #[cfg(feature = "iceberg-core")]
271 registry.register_table_source(
272 "iceberg",
273 info.clone(),
274 Arc::new(|config, declared_schema| {
275 Ok(Box::new(
276 IcebergReferenceTableSource::from_connector_config(config, declared_schema)?,
277 ))
278 }),
279 )?;
280
281 #[cfg(feature = "iceberg-core")]
283 registry.register_lookup_source("iceberg", info, Arc::new(IcebergLookupFactory))?;
284
285 Ok(())
286}
287
288#[cfg(feature = "iceberg-core")]
289struct IcebergLookupFactory;
290
291#[cfg(feature = "iceberg-core")]
292#[async_trait::async_trait]
293impl crate::registry::LookupSourceFactory for IcebergLookupFactory {
294 async fn build(
295 &self,
296 config: crate::config::ConnectorConfig,
297 _declared_schema: Option<arrow_schema::SchemaRef>,
298 ) -> Result<Arc<dyn laminar_core::lookup::source::LookupSourceDyn>, crate::error::ConnectorError>
299 {
300 use crate::lakehouse::iceberg_config::{IcebergCatalogConfig, IcebergStorageConfig};
301 let pk_columns: Vec<String> = config
302 .get("_primary_key_columns")
303 .unwrap_or("")
304 .split(',')
305 .map(|s| s.trim().to_string())
306 .filter(|s| !s.is_empty())
307 .collect();
308
309 if pk_columns.is_empty() {
310 return Err(crate::error::ConnectorError::ConfigurationError(
311 "iceberg lookup source requires primary key columns".into(),
312 ));
313 }
314
315 let catalog = IcebergCatalogConfig::from_config(&config)?;
316 let storage = IcebergStorageConfig::from_config(&config)?;
317 let lookup_config = IcebergLookupSourceConfig {
318 catalog,
319 storage,
320 primary_key_columns: pk_columns,
321 };
322
323 let source = IcebergLookupSource::open(lookup_config).await?;
324 Ok(Arc::new(source) as Arc<dyn laminar_core::lookup::source::LookupSourceDyn>)
325 }
326}
327
328pub fn register_lakehouse_sinks(
334 registry: &ConnectorRegistry,
335) -> Result<(), crate::error::ConnectorError> {
336 register_delta_lake_sink(registry)?;
337 register_iceberg_sink(registry)
338}
339
340pub fn register_lakehouse_sources(
346 registry: &ConnectorRegistry,
347) -> Result<(), crate::error::ConnectorError> {
348 register_delta_lake_source(registry)?;
349 register_iceberg_source(registry)
350}
351
352#[allow(clippy::too_many_lines)]
353fn delta_lake_config_keys() -> Vec<ConfigKeySpec> {
354 vec![
355 ConfigKeySpec::required(
356 "table.path",
357 "Path to Delta Lake table (local, s3://, az://, gs://)",
358 ),
359 ConfigKeySpec::optional(
360 "partition.columns",
361 "Comma-separated partition column names",
362 "",
363 ),
364 ConfigKeySpec::optional(
365 "target.file.size",
366 "Target Parquet file size in bytes",
367 "134217728",
368 ),
369 ConfigKeySpec::optional(
370 "max.buffer.records",
371 "Maximum records to buffer before flushing",
372 "100000",
373 ),
374 ConfigKeySpec::optional(
375 "max.buffer.duration.ms",
376 "Maximum time to buffer before flushing (ms)",
377 "60000",
378 ),
379 ConfigKeySpec::optional(
380 "schema.evolution",
381 "Enable automatic schema evolution (additive columns)",
382 "false",
383 ),
384 ConfigKeySpec::optional(
385 "write.mode",
386 "Write mode: append, overwrite, upsert",
387 "append",
388 ),
389 ConfigKeySpec::optional(
390 "merge.key.columns",
391 "Key columns for upsert MERGE (required for upsert mode)",
392 "",
393 ),
394 ConfigKeySpec::optional("catalog.type", "Catalog type: none, glue, unity", "none"),
396 ConfigKeySpec::optional(
397 "catalog.database",
398 "Catalog database name (required for Glue)",
399 "",
400 ),
401 ConfigKeySpec::optional("catalog.name", "Catalog name (required for Unity)", ""),
402 ConfigKeySpec::optional(
403 "catalog.schema",
404 "Catalog schema name (required for Unity)",
405 "",
406 ),
407 ConfigKeySpec::optional(
408 "catalog.workspace_url",
409 "Databricks workspace URL (required for Unity)",
410 "",
411 ),
412 ConfigKeySpec::optional(
413 "catalog.access_token",
414 "Databricks access token (required for Unity)",
415 "",
416 ),
417 ConfigKeySpec::optional(
418 "catalog.storage.location",
419 "Storage location for auto-created UC external tables (e.g. s3://bucket/path)",
420 "",
421 ),
422 ConfigKeySpec::optional(
424 "storage.s3_locking_provider",
425 "S3 locking provider: 'dynamodb' for DynamoDB-backed log store",
426 "",
427 ),
428 ConfigKeySpec::optional(
429 "storage.dynamodb_table_name",
430 "DynamoDB table name for S3 locking (default: delta_log)",
431 "",
432 ),
433 ConfigKeySpec::optional(
435 "storage.aws_access_key_id",
436 "AWS access key ID (falls back to AWS_ACCESS_KEY_ID env var)",
437 "",
438 ),
439 ConfigKeySpec::optional(
440 "storage.aws_secret_access_key",
441 "AWS secret access key (falls back to AWS_SECRET_ACCESS_KEY env var)",
442 "",
443 ),
444 ConfigKeySpec::optional(
445 "storage.aws_region",
446 "AWS region for S3 paths (falls back to AWS_REGION env var)",
447 "",
448 ),
449 ConfigKeySpec::optional(
450 "storage.aws_session_token",
451 "AWS session token for temporary credentials (falls back to AWS_SESSION_TOKEN)",
452 "",
453 ),
454 ConfigKeySpec::optional(
455 "storage.aws_endpoint",
456 "Custom S3 endpoint (MinIO, LocalStack; falls back to AWS_ENDPOINT_URL)",
457 "",
458 ),
459 ConfigKeySpec::optional(
460 "storage.aws_profile",
461 "AWS profile name (falls back to AWS_PROFILE env var)",
462 "",
463 ),
464 ConfigKeySpec::optional(
465 "storage.azure_storage_account_name",
466 "Azure storage account name (falls back to AZURE_STORAGE_ACCOUNT_NAME)",
467 "",
468 ),
469 ConfigKeySpec::optional(
470 "storage.azure_storage_account_key",
471 "Azure storage account key (falls back to AZURE_STORAGE_ACCOUNT_KEY)",
472 "",
473 ),
474 ConfigKeySpec::optional(
475 "storage.azure_storage_sas_token",
476 "Azure SAS token (falls back to AZURE_STORAGE_SAS_TOKEN)",
477 "",
478 ),
479 ConfigKeySpec::optional(
480 "storage.azure_storage_client_id",
481 "Azure client ID for service principal auth (falls back to AZURE_CLIENT_ID)",
482 "",
483 ),
484 ConfigKeySpec::optional(
485 "storage.google_service_account_path",
486 "Path to GCS service account JSON (falls back to GOOGLE_APPLICATION_CREDENTIALS)",
487 "",
488 ),
489 ConfigKeySpec::optional(
490 "storage.google_service_account_key",
491 "Inline GCS service account JSON (falls back to GOOGLE_SERVICE_ACCOUNT_KEY)",
492 "",
493 ),
494 ]
495}
496
497fn delta_lake_source_config_keys() -> Vec<ConfigKeySpec> {
498 vec![
499 ConfigKeySpec::required(
500 "table.path",
501 "Path to Delta Lake table (local, s3://, az://, gs://)",
502 ),
503 ConfigKeySpec::optional(
504 "starting.version",
505 "First version to read (default: only versions committed after startup)",
506 "",
507 ),
508 ConfigKeySpec::optional(
509 "poll.interval.ms",
510 "How often to poll for new versions (ms)",
511 "1000",
512 ),
513 ConfigKeySpec::optional("catalog.type", "Catalog type: none, glue, unity", "none"),
515 ConfigKeySpec::optional(
516 "catalog.database",
517 "Catalog database name (required for Glue)",
518 "",
519 ),
520 ConfigKeySpec::optional("catalog.name", "Catalog name (required for Unity)", ""),
521 ConfigKeySpec::optional(
522 "catalog.schema",
523 "Catalog schema name (required for Unity)",
524 "",
525 ),
526 ConfigKeySpec::optional(
527 "catalog.workspace_url",
528 "Databricks workspace URL (required for Unity)",
529 "",
530 ),
531 ConfigKeySpec::optional(
532 "catalog.access_token",
533 "Databricks access token (required for Unity)",
534 "",
535 ),
536 ConfigKeySpec::optional(
538 "storage.s3_locking_provider",
539 "S3 locking provider: 'dynamodb' for DynamoDB-backed log store",
540 "",
541 ),
542 ConfigKeySpec::optional(
543 "storage.dynamodb_table_name",
544 "DynamoDB table name for S3 locking (default: delta_log)",
545 "",
546 ),
547 ConfigKeySpec::optional("storage.aws_access_key_id", "AWS access key ID", ""),
549 ConfigKeySpec::optional("storage.aws_secret_access_key", "AWS secret access key", ""),
550 ConfigKeySpec::optional("storage.aws_region", "AWS region for S3 paths", ""),
551 ConfigKeySpec::optional(
552 "storage.azure_storage_account_name",
553 "Azure storage account name",
554 "",
555 ),
556 ConfigKeySpec::optional(
557 "storage.azure_storage_account_key",
558 "Azure storage account key",
559 "",
560 ),
561 ConfigKeySpec::optional(
562 "storage.google_service_account_path",
563 "Path to GCS service account JSON",
564 "",
565 ),
566 ]
567}
568
569#[cfg(test)]
570mod tests;