faucet_source_oracle/
config.rs1use faucet_common_oracle::OracleConnectionConfig;
4use faucet_core::{DEFAULT_BATCH_SIZE, FaucetError, validate_batch_size};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9fn default_max_connections() -> u32 {
10 10
11}
12fn default_batch_size() -> usize {
13 DEFAULT_BATCH_SIZE
14}
15fn default_statement_timeout_secs() -> u64 {
16 300
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
24#[serde(tag = "type", rename_all = "snake_case")]
25pub enum OracleReplication {
26 #[default]
28 Full,
29 Incremental {
42 column: String,
44 initial_value: Value,
47 },
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
54pub struct ShardConfig {
55 pub key: String,
57}
58
59#[derive(Clone, Serialize, Deserialize, JsonSchema)]
61#[schemars(extend("x-faucet-follows-servers" = true))]
62pub struct OracleSourceConfig {
63 #[serde(flatten)]
65 pub connection: OracleConnectionConfig,
66 #[schemars(extend("x-faucet-sql" = true))]
69 pub query: String,
70 #[serde(default)]
72 pub params: Vec<Value>,
73 #[serde(default = "default_max_connections")]
75 pub max_connections: u32,
76 #[serde(default = "default_batch_size")]
79 pub batch_size: usize,
80 #[serde(default = "default_statement_timeout_secs")]
82 pub statement_timeout_secs: u64,
83 #[serde(default)]
85 pub replication: OracleReplication,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub state_key: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub shard: Option<ShardConfig>,
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub json_columns: Vec<String>,
98}
99
100impl std::fmt::Debug for OracleSourceConfig {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("OracleSourceConfig")
103 .field("connection", &self.connection)
104 .field("query", &self.query)
105 .field("params", &self.params)
106 .field("max_connections", &self.max_connections)
107 .field("batch_size", &self.batch_size)
108 .field("statement_timeout_secs", &self.statement_timeout_secs)
109 .field("replication", &self.replication)
110 .field("state_key", &self.state_key)
111 .field("shard", &self.shard)
112 .field("json_columns", &self.json_columns)
113 .finish()
114 }
115}
116
117impl OracleSourceConfig {
118 pub fn new(connection: OracleConnectionConfig, query: impl Into<String>) -> Self {
120 Self {
121 connection,
122 query: query.into(),
123 params: Vec::new(),
124 max_connections: default_max_connections(),
125 batch_size: default_batch_size(),
126 statement_timeout_secs: default_statement_timeout_secs(),
127 replication: OracleReplication::Full,
128 state_key: None,
129 shard: None,
130 json_columns: Vec::new(),
131 }
132 }
133
134 pub fn validate(&self) -> Result<(), FaucetError> {
136 self.connection.validate()?;
137 validate_batch_size(self.batch_size)?;
138 if self.query.trim().is_empty() {
139 return Err(FaucetError::Config(
140 "oracle source requires a `query`".into(),
141 ));
142 }
143 if let OracleReplication::Incremental {
144 column,
145 initial_value,
146 } = &self.replication
147 {
148 if column.trim().is_empty() {
149 return Err(FaucetError::Config(
150 "oracle incremental replication requires a non-empty `column`".into(),
151 ));
152 }
153 faucet_common_oracle::quote_ident_oracle(column)?;
154 if initial_value.is_null() {
155 return Err(FaucetError::Config(
156 "oracle incremental replication requires a non-null `initial_value`".into(),
157 ));
158 }
159 if faucet_core::replication::cursor::strict_comparison(
160 &self.query.to_ascii_lowercase(),
161 BOOKMARK_TOKEN,
162 ) {
163 return Err(FaucetError::Config(format!(
164 "oracle: compare `{BOOKMARK_TOKEN}` with `>=`, not `>`: the source re-reads \
165 the bookmark value and drops the rows it already emitted, and a strict \
166 comparison skips rows that share it"
167 )));
168 }
169 }
170 if let Some(shard) = &self.shard {
171 faucet_common_oracle::quote_ident_oracle(&shard.key)?;
172 }
173 if let Some(key) = &self.state_key {
174 faucet_core::state::validate_state_key(key)?;
175 }
176 Ok(())
177 }
178}
179
180pub const BOOKMARK_TOKEN: &str = ":bookmark";
183
184#[cfg(test)]
185mod tests;