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
37#[cfg(any(feature = "delta-lake", feature = "iceberg-core"))]
38mod schema_resolution;
39
40pub mod metrics;
42#[cfg(any(test, feature = "delta-lake", feature = "iceberg-core"))]
43mod snapshot_schema;
44
45pub use delta::DeltaLakeSink;
47pub use delta_config::{DeltaCatalogType, DeltaLakeSinkConfig, DeltaWriteMode};
48#[cfg(feature = "delta-lake")]
49pub use delta_lookup::{DeltaLookupSource, DeltaLookupSourceConfig};
50pub use delta_metrics::DeltaLakeSinkMetrics;
51#[cfg(feature = "delta-lake")]
52pub use delta_reference::DeltaReferenceTableSource;
53pub use delta_source::DeltaSource;
54pub use delta_source_config::DeltaSourceConfig;
55pub use metrics::LakehouseSinkMetrics;
56
57pub use iceberg::IcebergSink;
59use iceberg_config::{
60 sink_config_keys as iceberg_sink_config_keys, source_config_keys as iceberg_source_config_keys,
61};
62pub use iceberg_config::{
63 IcebergCatalogAuthType, IcebergCatalogConfig, IcebergCatalogType, IcebergNullOrder,
64 IcebergPartitionField, IcebergReadBootstrap, IcebergReadMode, IcebergSchemaEvolutionMode,
65 IcebergSinkConfig, IcebergSortDirection, IcebergSortField, IcebergSourceConfig,
66 IcebergStorageConfig, IcebergStorageEncryption, IcebergStorageType, IcebergTransform,
67 IcebergWriteDistributionMode, IcebergWriteMode,
68};
69#[cfg(feature = "iceberg-core")]
70pub use iceberg_lookup::{IcebergLookupSource, IcebergLookupSourceConfig};
71#[cfg(feature = "iceberg-core")]
72pub use iceberg_reference::IcebergReferenceTableSource;
73pub use iceberg_source::IcebergSource;
74
75use std::sync::Arc;
76
77use crate::config::{ConfigKeySpec, ConnectorInfo};
78use crate::registry::ConnectorRegistry;
79
80pub fn register_delta_lake_sink(
86 registry: &ConnectorRegistry,
87) -> Result<(), crate::error::ConnectorError> {
88 let info = ConnectorInfo {
89 schema_capabilities: crate::schema::resolution::SchemaCapabilities::metadata(
90 &[],
91 true,
92 crate::schema::resolution::SchemaPreparation::ExplicitTableCreation,
93 ),
94 name: "delta-lake".to_string(),
95 display_name: "Delta Lake Sink".to_string(),
96 version: env!("CARGO_PKG_VERSION").to_string(),
97 is_source: false,
98 is_sink: true,
99 config_keys: delta_lake_config_keys(),
100 };
101
102 registry.register_sink(
103 "delta-lake",
104 info,
105 Arc::new(|config, registry: Option<&Arc<prometheus::Registry>>| {
106 Ok(Box::new(DeltaLakeSink::new(
107 DeltaLakeSinkConfig::from_config(config)?,
108 registry.map(Arc::as_ref),
109 )))
110 }),
111 )
112}
113
114pub fn register_delta_lake_source(
120 registry: &ConnectorRegistry,
121) -> Result<(), crate::error::ConnectorError> {
122 let info = ConnectorInfo {
123 schema_capabilities: crate::schema::resolution::SchemaCapabilities::metadata(
124 &[],
125 false,
126 crate::schema::resolution::SchemaPreparation::None,
127 ),
128 name: "delta-lake".to_string(),
129 display_name: "Delta Lake Source".to_string(),
130 version: env!("CARGO_PKG_VERSION").to_string(),
131 is_source: true,
132 is_sink: false,
133 config_keys: delta_lake_source_config_keys(),
134 };
135
136 registry.register_source(
137 "delta-lake",
138 info.clone(),
139 Arc::new(|registry: Option<&Arc<prometheus::Registry>>| {
140 Ok(Box::new(DeltaSource::new(
141 DeltaSourceConfig::default(),
142 registry.map(Arc::as_ref),
143 )))
144 }),
145 )?;
146
147 #[cfg(feature = "delta-lake")]
149 registry.register_table_source(
150 "delta-lake",
151 info.clone(),
152 Arc::new(|config, declared_schema| {
153 Ok(Box::new(DeltaReferenceTableSource::from_connector_config(
154 config,
155 declared_schema,
156 )?))
157 }),
158 )?;
159
160 #[cfg(feature = "delta-lake")]
162 registry.register_lookup_source("delta-lake", info, Arc::new(DeltaLookupFactory))?;
163
164 Ok(())
165}
166
167#[cfg(feature = "delta-lake")]
168struct DeltaLookupFactory;
169
170#[cfg(feature = "delta-lake")]
171#[async_trait::async_trait]
172impl crate::registry::LookupSourceFactory for DeltaLookupFactory {
173 async fn build(
174 &self,
175 config: crate::config::ConnectorConfig,
176 _declared_schema: Option<arrow_schema::SchemaRef>,
177 ) -> Result<Arc<dyn laminar_core::lookup::source::LookupSourceDyn>, crate::error::ConnectorError>
178 {
179 use crate::lakehouse::delta_source_config::DeltaSourceConfig;
180 let pk_columns: Vec<String> = config
181 .get("_primary_key_columns")
182 .unwrap_or("")
183 .split(',')
184 .map(|s| s.trim().to_string())
185 .filter(|s| !s.is_empty())
186 .collect();
187
188 if pk_columns.is_empty() {
189 return Err(crate::error::ConnectorError::ConfigurationError(
190 "delta-lake lookup source requires primary key columns".into(),
191 ));
192 }
193
194 let src_config = DeltaSourceConfig::from_config(&config)?;
195
196 let resolved = crate::lakehouse::delta_io::resolve_catalog_options(
197 &src_config.catalog_type,
198 src_config.catalog_database.as_deref(),
199 src_config.catalog_name.as_deref(),
200 src_config.catalog_schema.as_deref(),
201 &src_config.table_path,
202 &src_config.storage_options,
203 crate::lakehouse::delta_io::CatalogAccess::Read,
204 )
205 .await?;
206 if resolved.credentials_expire_at.is_some() {
207 return Err(crate::error::ConnectorError::ConfigurationError(
208 "on-demand Delta lookups keep the storage credentials they open with and cannot \
209 renew Unity Catalog vended credentials; use a reference table, or \
210 catalog.storage.credentials=configured"
211 .into(),
212 ));
213 }
214
215 let lookup_config = DeltaLookupSourceConfig {
216 table_path: resolved.location,
217 storage_options: resolved.storage_options,
218 primary_key_columns: pk_columns,
219 table_name: "delta_lookup".to_string(),
220 };
221
222 let source = DeltaLookupSource::open(lookup_config).await?;
224
225 Ok(Arc::new(source) as Arc<dyn laminar_core::lookup::source::LookupSourceDyn>)
226 }
227}
228
229pub fn register_iceberg_sink(
235 registry: &ConnectorRegistry,
236) -> Result<(), crate::error::ConnectorError> {
237 let info = ConnectorInfo {
238 schema_capabilities: crate::schema::resolution::SchemaCapabilities::metadata(
239 &[],
240 true,
241 crate::schema::resolution::SchemaPreparation::ExplicitTableCreation,
242 ),
243 name: "iceberg".to_string(),
244 display_name: "Apache Iceberg Sink".to_string(),
245 version: env!("CARGO_PKG_VERSION").to_string(),
246 is_source: false,
247 is_sink: true,
248 config_keys: iceberg_sink_config_keys(),
249 };
250
251 registry.register_sink(
252 "iceberg",
253 info,
254 Arc::new(|config, registry: Option<&Arc<prometheus::Registry>>| {
255 Ok(Box::new(IcebergSink::new(
256 IcebergSinkConfig::from_config(config)?,
257 registry.map(Arc::as_ref),
258 )))
259 }),
260 )
261}
262
263pub fn register_iceberg_source(
269 registry: &ConnectorRegistry,
270) -> Result<(), crate::error::ConnectorError> {
271 let info = ConnectorInfo {
272 schema_capabilities: crate::schema::resolution::SchemaCapabilities::metadata(
273 &[],
274 false,
275 crate::schema::resolution::SchemaPreparation::None,
276 ),
277 name: "iceberg".to_string(),
278 display_name: "Apache Iceberg Source".to_string(),
279 version: env!("CARGO_PKG_VERSION").to_string(),
280 is_source: true,
281 is_sink: false,
282 config_keys: iceberg_source_config_keys(),
283 };
284
285 registry.register_source(
286 "iceberg",
287 info.clone(),
288 Arc::new(|registry: Option<&Arc<prometheus::Registry>>| {
289 let mut config = crate::config::ConnectorConfig::new("iceberg");
290 config.set("catalog.uri", "http://localhost:8181");
291 config.set("catalog.warehouse", "s3://default/wh");
292 config.set("namespace", "default");
293 config.set("table.name", "default");
294 Ok(Box::new(IcebergSource::new(
295 IcebergSourceConfig::from_config(&config)?,
296 registry.map(Arc::as_ref),
297 )))
298 }),
299 )?;
300
301 #[cfg(feature = "iceberg-core")]
303 registry.register_table_source(
304 "iceberg",
305 info.clone(),
306 Arc::new(|config, declared_schema| {
307 Ok(Box::new(
308 IcebergReferenceTableSource::from_connector_config(config, declared_schema)?,
309 ))
310 }),
311 )?;
312
313 #[cfg(feature = "iceberg-core")]
315 registry.register_lookup_source("iceberg", info, Arc::new(IcebergLookupFactory))?;
316
317 Ok(())
318}
319
320#[cfg(feature = "iceberg-core")]
321struct IcebergLookupFactory;
322
323#[cfg(feature = "iceberg-core")]
324#[async_trait::async_trait]
325impl crate::registry::LookupSourceFactory for IcebergLookupFactory {
326 async fn build(
327 &self,
328 config: crate::config::ConnectorConfig,
329 _declared_schema: Option<arrow_schema::SchemaRef>,
330 ) -> Result<Arc<dyn laminar_core::lookup::source::LookupSourceDyn>, crate::error::ConnectorError>
331 {
332 use crate::lakehouse::iceberg_config::{IcebergCatalogConfig, IcebergStorageConfig};
333 let pk_columns: Vec<String> = config
334 .get("_primary_key_columns")
335 .unwrap_or("")
336 .split(',')
337 .map(|s| s.trim().to_string())
338 .filter(|s| !s.is_empty())
339 .collect();
340
341 if pk_columns.is_empty() {
342 return Err(crate::error::ConnectorError::ConfigurationError(
343 "iceberg lookup source requires primary key columns".into(),
344 ));
345 }
346
347 let catalog = IcebergCatalogConfig::from_config(&config)?;
348 let storage = IcebergStorageConfig::from_config(&config)?;
349 let lookup_config = IcebergLookupSourceConfig {
350 catalog,
351 storage,
352 primary_key_columns: pk_columns,
353 };
354
355 let source = IcebergLookupSource::open(lookup_config).await?;
356 Ok(Arc::new(source) as Arc<dyn laminar_core::lookup::source::LookupSourceDyn>)
357 }
358}
359
360pub fn register_lakehouse_sinks(
366 registry: &ConnectorRegistry,
367) -> Result<(), crate::error::ConnectorError> {
368 register_delta_lake_sink(registry)?;
369 register_iceberg_sink(registry)
370}
371
372pub fn register_lakehouse_sources(
378 registry: &ConnectorRegistry,
379) -> Result<(), crate::error::ConnectorError> {
380 register_delta_lake_source(registry)?;
381 register_iceberg_source(registry)
382}
383
384fn delta_lake_config_keys() -> Vec<ConfigKeySpec> {
385 let mut keys = vec![
386 ConfigKeySpec::required(
387 "table.path",
388 "Path to Delta Lake table (local, s3://, az://, gs://)",
389 ),
390 ConfigKeySpec::optional(
391 "partition.columns",
392 "Comma-separated partition column names",
393 "",
394 ),
395 ConfigKeySpec::optional(
396 "target.file.size",
397 "Target Parquet file size in bytes",
398 "134217728",
399 ),
400 ConfigKeySpec::optional(
401 "max.buffer.records",
402 "Maximum records to buffer before flushing",
403 "100000",
404 ),
405 ConfigKeySpec::optional(
406 "max.buffer.duration.ms",
407 "Maximum time to buffer before flushing (ms)",
408 "60000",
409 ),
410 ConfigKeySpec::optional(
411 "schema.evolution",
412 "Enable automatic schema evolution (additive columns)",
413 "false",
414 ),
415 ConfigKeySpec::optional(
416 "write.mode",
417 "Write mode: append, overwrite (each flush replaces the whole table), upsert",
418 "append",
419 ),
420 ConfigKeySpec::optional(
421 "merge.key.columns",
422 "Key columns for upsert MERGE (required for upsert mode)",
423 "",
424 ),
425 ];
426 keys.extend(delta_catalog_config_keys());
427 keys.extend([
428 ConfigKeySpec::optional(
429 "catalog.storage.location",
430 "Storage location for auto-created UC external tables (e.g. s3://bucket/path)",
431 "",
432 ),
433 ConfigKeySpec::optional(
435 "storage.s3_locking_provider",
436 "S3 locking provider: 'dynamodb' for DynamoDB-backed log store",
437 "",
438 ),
439 ConfigKeySpec::optional(
440 "storage.dynamodb_table_name",
441 "DynamoDB table name for S3 locking (default: delta_log)",
442 "",
443 ),
444 ConfigKeySpec::optional(
446 "storage.aws_access_key_id",
447 "AWS access key ID (falls back to AWS_ACCESS_KEY_ID env var)",
448 "",
449 ),
450 ConfigKeySpec::optional(
451 "storage.aws_secret_access_key",
452 "AWS secret access key (falls back to AWS_SECRET_ACCESS_KEY env var)",
453 "",
454 ),
455 ConfigKeySpec::optional(
456 "storage.aws_region",
457 "AWS region for S3 paths (falls back to AWS_REGION env var)",
458 "",
459 ),
460 ConfigKeySpec::optional(
461 "storage.aws_session_token",
462 "AWS session token for temporary credentials (falls back to AWS_SESSION_TOKEN)",
463 "",
464 ),
465 ConfigKeySpec::optional(
466 "storage.aws_endpoint",
467 "Custom S3 endpoint (MinIO, LocalStack; falls back to AWS_ENDPOINT_URL)",
468 "",
469 ),
470 ConfigKeySpec::optional(
471 "storage.aws_profile",
472 "AWS profile name (falls back to AWS_PROFILE env var)",
473 "",
474 ),
475 ConfigKeySpec::optional(
476 "storage.azure_storage_account_name",
477 "Azure storage account name (falls back to AZURE_STORAGE_ACCOUNT_NAME)",
478 "",
479 ),
480 ConfigKeySpec::optional(
481 "storage.azure_storage_account_key",
482 "Azure storage account key (falls back to AZURE_STORAGE_ACCOUNT_KEY)",
483 "",
484 ),
485 ConfigKeySpec::optional(
486 "storage.azure_storage_sas_token",
487 "Azure SAS token (falls back to AZURE_STORAGE_SAS_TOKEN)",
488 "",
489 ),
490 ConfigKeySpec::optional(
491 "storage.azure_storage_client_id",
492 "Azure client ID for service principal auth (falls back to AZURE_CLIENT_ID)",
493 "",
494 ),
495 ConfigKeySpec::optional(
496 "storage.google_service_account_path",
497 "Path to GCS service account JSON (falls back to GOOGLE_APPLICATION_CREDENTIALS)",
498 "",
499 ),
500 ConfigKeySpec::optional(
501 "storage.google_service_account_key",
502 "Inline GCS service account JSON (falls back to GOOGLE_SERVICE_ACCOUNT_KEY)",
503 "",
504 ),
505 ]);
506 keys
507}
508
509fn delta_catalog_config_keys() -> Vec<ConfigKeySpec> {
511 vec![
512 ConfigKeySpec::optional("catalog.type", "Catalog type: none, glue, unity", "none"),
513 ConfigKeySpec::optional(
514 "catalog.database",
515 "Catalog database name (required for Glue)",
516 "",
517 ),
518 ConfigKeySpec::optional("catalog.name", "Catalog name (required for Unity)", ""),
519 ConfigKeySpec::optional(
520 "catalog.schema",
521 "Catalog schema name (required for Unity)",
522 "",
523 ),
524 ConfigKeySpec::optional(
525 "catalog.workspace_url",
526 "Databricks workspace URL (required for Unity)",
527 "",
528 ),
529 ConfigKeySpec::optional(
530 "catalog.access_token",
531 "Unity Catalog token (PAT or service-principal token); or use client_id/client_secret",
532 "",
533 ),
534 ConfigKeySpec::optional(
535 "catalog.client_id",
536 "Unity Catalog OAuth M2M service-principal client id",
537 "",
538 ),
539 ConfigKeySpec::optional(
540 "catalog.client_secret",
541 "Unity Catalog OAuth M2M service-principal secret",
542 "",
543 ),
544 ConfigKeySpec::optional(
545 "catalog.storage.credentials",
546 "Unity storage credentials: vended (table-scoped, renewed) or configured (storage.*)",
547 "vended",
548 ),
549 ]
550}
551
552fn delta_lake_source_config_keys() -> Vec<ConfigKeySpec> {
553 let mut keys = vec![
554 ConfigKeySpec::required(
555 "table.path",
556 "Path to Delta Lake table (local, s3://, az://, gs://)",
557 ),
558 ConfigKeySpec::optional(
559 "read.mode",
560 "changes (snapshot/history then the change data feed) or snapshot (one snapshot, then complete)",
561 "changes",
562 ),
563 ConfigKeySpec::optional(
564 "starting.version",
565 "Changes mode: first change version to read (inclusive), or 'latest' for commits after \
566 first start; unset emits the latest snapshot, then its later changes",
567 "",
568 ),
569 ConfigKeySpec::optional(
570 "poll.interval.ms",
571 "How often to poll for new versions (ms)",
572 "1000",
573 ),
574 ];
575 keys.extend(delta_catalog_config_keys());
576 keys.extend([
577 ConfigKeySpec::optional(
579 "storage.s3_locking_provider",
580 "S3 locking provider: 'dynamodb' for DynamoDB-backed log store",
581 "",
582 ),
583 ConfigKeySpec::optional(
584 "storage.dynamodb_table_name",
585 "DynamoDB table name for S3 locking (default: delta_log)",
586 "",
587 ),
588 ConfigKeySpec::optional("storage.aws_access_key_id", "AWS access key ID", ""),
590 ConfigKeySpec::optional("storage.aws_secret_access_key", "AWS secret access key", ""),
591 ConfigKeySpec::optional("storage.aws_region", "AWS region for S3 paths", ""),
592 ConfigKeySpec::optional(
593 "storage.azure_storage_account_name",
594 "Azure storage account name",
595 "",
596 ),
597 ConfigKeySpec::optional(
598 "storage.azure_storage_account_key",
599 "Azure storage account key",
600 "",
601 ),
602 ConfigKeySpec::optional(
603 "storage.google_service_account_path",
604 "Path to GCS service account JSON",
605 "",
606 ),
607 ]);
608 keys
609}
610
611#[cfg(test)]
612mod tests;