faucet_sink_delta/
config.rs1use faucet_common_delta::DeltaConnection;
4use faucet_core::{DEFAULT_BATCH_SIZE, validate_batch_size};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8pub const DEFAULT_SAMPLE_SIZE: usize = 100;
10
11fn default_true() -> bool {
12 true
13}
14
15fn default_sample_size() -> usize {
16 DEFAULT_SAMPLE_SIZE
17}
18
19fn default_batch_size() -> usize {
20 DEFAULT_BATCH_SIZE
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
30pub struct DeltaSinkConfig {
31 #[serde(flatten)]
33 pub connection: DeltaConnection,
34
35 #[serde(default = "default_true")]
39 pub create_if_not_missing: bool,
40
41 #[serde(default)]
44 pub partition_by: Vec<String>,
45
46 #[serde(default = "default_sample_size")]
48 pub schema_sample_size: usize,
49
50 #[serde(default = "default_batch_size")]
54 pub batch_size: usize,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub target_file_size: Option<usize>,
60}
61
62impl DeltaSinkConfig {
63 pub fn new(table_uri: impl Into<String>) -> Self {
65 Self {
66 connection: DeltaConnection {
67 table_uri: table_uri.into(),
68 credentials: Default::default(),
69 storage_options: Default::default(),
70 },
71 create_if_not_missing: true,
72 partition_by: Vec::new(),
73 schema_sample_size: DEFAULT_SAMPLE_SIZE,
74 batch_size: DEFAULT_BATCH_SIZE,
75 target_file_size: None,
76 }
77 }
78
79 pub fn validate(&self) -> Result<(), String> {
81 if self.connection.table_uri.trim().is_empty() {
82 return Err("delta sink: `table_uri` must not be empty".to_string());
83 }
84 if self.schema_sample_size == 0 {
85 return Err("delta sink: `schema_sample_size` must be greater than 0".to_string());
86 }
87 if matches!(self.target_file_size, Some(0)) {
88 return Err(
89 "delta sink: `target_file_size` must be greater than 0 when set".to_string(),
90 );
91 }
92 validate_batch_size(self.batch_size)
93 .map_err(|e| format!("delta sink: invalid batch_size: {e}"))?;
94 Ok(())
95 }
96
97 pub fn effective_sample_size(&self) -> usize {
99 self.schema_sample_size.max(1)
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use serde_json::json;
107
108 #[test]
109 fn new_has_sane_defaults() {
110 let c = DeltaSinkConfig::new("file:///tmp/t");
111 assert!(c.create_if_not_missing);
112 assert_eq!(c.schema_sample_size, DEFAULT_SAMPLE_SIZE);
113 assert_eq!(c.batch_size, DEFAULT_BATCH_SIZE);
114 assert!(c.partition_by.is_empty());
115 c.validate().unwrap();
116 }
117
118 #[test]
119 fn rejects_empty_uri_and_zero_sample() {
120 assert!(DeltaSinkConfig::new("").validate().is_err());
121 let mut c = DeltaSinkConfig::new("file:///tmp/t");
122 c.schema_sample_size = 0;
123 assert!(c.validate().is_err());
124 }
125
126 #[test]
127 fn rejects_zero_target_file_size() {
128 let mut c = DeltaSinkConfig::new("file:///tmp/t");
129 c.target_file_size = Some(0);
130 assert!(c.validate().is_err());
131 }
132
133 #[test]
134 fn rejects_oversized_batch() {
135 let mut c = DeltaSinkConfig::new("file:///tmp/t");
136 c.batch_size = faucet_core::MAX_BATCH_SIZE + 1;
137 assert!(c.validate().is_err());
138 }
139
140 #[test]
141 fn deserializes_minimal_applies_defaults() {
142 let c: DeltaSinkConfig =
144 serde_json::from_value(json!({ "table_uri": "file:///t" })).unwrap();
145 assert!(c.create_if_not_missing);
146 assert_eq!(c.schema_sample_size, DEFAULT_SAMPLE_SIZE);
147 assert_eq!(c.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
148 assert_eq!(c.effective_sample_size(), DEFAULT_SAMPLE_SIZE);
149 assert!(c.target_file_size.is_none());
150 c.validate().unwrap();
151 }
152
153 #[test]
154 fn deserializes_flattened_shape_with_partitions() {
155 let v = json!({
156 "table_uri": "s3://b/t",
157 "partition_by": ["dt"],
158 "create_if_not_missing": false,
159 "storage_options": { "AWS_ALLOW_HTTP": "true" }
160 });
161 let c: DeltaSinkConfig = serde_json::from_value(v).unwrap();
162 assert_eq!(c.partition_by, vec!["dt"]);
163 assert!(!c.create_if_not_missing);
164 assert_eq!(
165 c.connection.merged_storage_options()["AWS_ALLOW_HTTP"],
166 "true"
167 );
168 c.validate().unwrap();
169 }
170}