faucet_source_oracle_cdc/
config.rs1use std::time::Duration;
4
5use faucet_common_oracle::{OracleConnectionConfig, quote_ident_oracle, split_table};
6use faucet_core::{DEFAULT_BATCH_SIZE, FaucetError, validate_batch_size};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10fn default_poll_interval() -> Duration {
11 Duration::from_secs(1)
12}
13fn default_idle_timeout() -> Duration {
14 Duration::from_secs(30)
15}
16fn default_batch_size() -> usize {
17 DEFAULT_BATCH_SIZE
18}
19fn default_max_connections() -> u32 {
20 2
21}
22fn default_statement_timeout_secs() -> u64 {
23 600
24}
25fn default_max_scn_window() -> u64 {
26 500_000
27}
28fn default_flush_table() -> String {
29 "FAUCET_LOGMNR_FLUSH".into()
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
34#[serde(tag = "type", rename_all = "snake_case")]
35pub enum StartPosition {
36 #[default]
39 Current,
40 Earliest,
42}
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
48#[serde(rename_all = "snake_case")]
49pub enum OnUnsupported {
50 #[default]
52 Fail,
53 Skip,
55}
56
57#[derive(Clone, Serialize, Deserialize, JsonSchema)]
59#[schemars(extend("x-faucet-follows-servers" = true))]
60pub struct OracleCdcSourceConfig {
61 #[serde(flatten)]
64 pub connection: OracleConnectionConfig,
65 pub tables: Vec<String>,
68 #[serde(default)]
70 pub start_position: StartPosition,
71 #[serde(
73 default = "default_poll_interval",
74 with = "faucet_core::config::duration_secs"
75 )]
76 #[schemars(with = "u64")]
77 pub poll_interval: Duration,
78 #[serde(
82 default = "default_idle_timeout",
83 with = "faucet_core::config::duration_secs"
84 )]
85 #[schemars(with = "u64")]
86 pub idle_timeout: Duration,
87 #[serde(default = "default_max_scn_window")]
89 pub max_scn_window: u64,
90 #[serde(default = "faucet_core::replication::default_max_staged_records")]
93 #[schemars(extend("x-faucet-caller-max" = 1_000_000))]
94 pub max_staged_records: Option<usize>,
95 #[serde(default)]
99 #[schemars(extend("x-faucet-caller-max" = 1_000_000, "x-faucet-caller-set" = true))]
100 pub max_staged_records_total: Option<usize>,
101 #[serde(default = "default_batch_size")]
106 pub batch_size: usize,
107 #[serde(default = "default_max_connections")]
109 pub max_connections: u32,
110 #[serde(default = "default_statement_timeout_secs")]
112 pub statement_timeout_secs: u64,
113 #[serde(default = "default_flush_table")]
117 pub flush_table: String,
118 #[serde(default)]
120 pub on_unsupported: OnUnsupported,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub state_key: Option<String>,
125}
126
127impl std::fmt::Debug for OracleCdcSourceConfig {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("OracleCdcSourceConfig")
130 .field("connection", &self.connection)
131 .field("tables", &self.tables)
132 .field("start_position", &self.start_position)
133 .field("poll_interval", &self.poll_interval)
134 .field("idle_timeout", &self.idle_timeout)
135 .field("max_scn_window", &self.max_scn_window)
136 .field("max_staged_records", &self.max_staged_records)
137 .field("max_staged_records_total", &self.max_staged_records_total)
138 .field("batch_size", &self.batch_size)
139 .field("max_connections", &self.max_connections)
140 .field("statement_timeout_secs", &self.statement_timeout_secs)
141 .field("flush_table", &self.flush_table)
142 .field("on_unsupported", &self.on_unsupported)
143 .field("state_key", &self.state_key)
144 .finish()
145 }
146}
147
148impl OracleCdcSourceConfig {
149 pub fn new(connection: OracleConnectionConfig, tables: Vec<String>) -> Self {
151 Self {
152 connection,
153 tables,
154 start_position: StartPosition::default(),
155 poll_interval: default_poll_interval(),
156 idle_timeout: default_idle_timeout(),
157 max_scn_window: default_max_scn_window(),
158 max_staged_records: faucet_core::replication::default_max_staged_records(),
159 max_staged_records_total: None,
160 batch_size: default_batch_size(),
161 max_connections: default_max_connections(),
162 statement_timeout_secs: default_statement_timeout_secs(),
163 flush_table: default_flush_table(),
164 on_unsupported: OnUnsupported::default(),
165 state_key: None,
166 }
167 }
168
169 pub fn validate(&self) -> Result<(), FaucetError> {
171 self.connection.validate()?;
172 if self.tables.is_empty() {
173 return Err(FaucetError::Config(
174 "oracle-cdc: `tables` must list at least one OWNER.TABLE".into(),
175 ));
176 }
177 let mut seen = std::collections::BTreeSet::new();
178 for t in &self.tables {
179 let (owner, name) = split_table(t)?;
180 let Some(owner) = owner else {
181 return Err(FaucetError::Config(format!(
182 "oracle-cdc: table {t:?} must be owner-qualified (OWNER.TABLE)"
183 )));
184 };
185 quote_ident_oracle(&owner)?;
186 quote_ident_oracle(&name)?;
187 if !seen.insert(t) {
188 return Err(FaucetError::Config(format!(
189 "oracle-cdc: duplicate table {t:?} in `tables`"
190 )));
191 }
192 }
193 quote_ident_oracle(&self.flush_table)?;
194 if self.poll_interval.is_zero() || self.idle_timeout.is_zero() {
195 return Err(FaucetError::Config(
196 "oracle-cdc: poll_interval and idle_timeout must be > 0".into(),
197 ));
198 }
199 if self.max_scn_window == 0 {
200 return Err(FaucetError::Config(
201 "oracle-cdc: max_scn_window must be > 0".into(),
202 ));
203 }
204 validate_batch_size(self.batch_size)?;
205 faucet_core::state::validate_state_key(&self.resolved_state_key())?;
206 Ok(())
207 }
208
209 pub fn table_pairs(&self) -> Vec<(String, String)> {
211 self.tables
212 .iter()
213 .filter_map(|t| match split_table(t) {
214 Ok((Some(o), n)) => Some((o, n)),
215 _ => None,
216 })
217 .collect()
218 }
219
220 pub fn resolved_state_key(&self) -> String {
222 if let Some(k) = &self.state_key {
223 return k.clone();
224 }
225 self.key_for_scope(self.connection.scope_label())
226 }
227
228 pub fn legacy_state_keys(&self) -> Vec<String> {
231 if self.state_key.is_some() {
232 return Vec::new();
233 }
234 self.connection
235 .legacy_scope_label()
236 .map(|scope| vec![self.key_for_scope(scope)])
237 .unwrap_or_default()
238 }
239
240 fn key_for_scope(&self, scope: String) -> String {
241 let scope = if scope.is_empty() {
242 "oracle".into()
243 } else {
244 scope
245 };
246 if self.tables.len() == 1 {
247 let t: String = self.tables[0]
248 .chars()
249 .map(|c| {
250 if c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.') {
251 c
252 } else {
253 '_'
254 }
255 })
256 .collect();
257 return format!("oracle-cdc:{scope}:{t}");
258 }
259 let mut sorted: Vec<&str> = self.tables.iter().map(String::as_str).collect();
260 sorted.sort_unstable();
261 format!("oracle-cdc:{scope}:{}", fnv1a_hex(&sorted.join(",")))
262 }
263}
264
265fn fnv1a_hex(s: &str) -> String {
266 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
267 for b in s.bytes() {
268 hash ^= u64::from(b);
269 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
270 }
271 format!("{hash:016x}")
272}
273
274#[cfg(test)]
275mod tests;