risingwave_connector/schema/
loader.rs1use std::collections::BTreeMap;
16
17use risingwave_pb::catalog::PbSchemaRegistryNameStrategy;
18
19use super::schema_registry::{
20 Client, Subject, get_subject_by_strategy, handle_sr_list, name_strategy_from_str,
21};
22use super::{
23 AWS_GLUE_SCHEMA_ARN_KEY, InvalidOptionError, KEY_MESSAGE_NAME_KEY, MESSAGE_NAME_KEY,
24 MalformedResponseError, NAME_STRATEGY_KEY, SCHEMA_REGISTRY_KEY, SchemaFetchError,
25 invalid_option_error, malformed_response_error,
26};
27use crate::connector_common::AwsAuthProps;
28
29pub enum SchemaLoader {
30 Confluent(ConfluentSchemaLoader),
31 Glue(GlueSchemaLoader),
32}
33
34pub struct ConfluentSchemaLoader {
35 pub client: Client,
36 pub name_strategy: PbSchemaRegistryNameStrategy,
37 pub topic: String,
38 pub key_record_name: Option<String>,
39 pub val_record_name: Option<String>,
40}
41
42pub enum GlueSchemaLoader {
43 Real {
44 client: aws_sdk_glue::Client,
45 schema_arn: String,
46 },
47 Mock {
48 schema_version_id: uuid::Uuid,
49 definition: String,
50 },
51}
52
53pub enum SchemaVersion {
54 Confluent(i32),
55 Glue(uuid::Uuid),
56}
57
58impl ConfluentSchemaLoader {
59 pub fn from_format_options(
60 topic: &str,
61 format_options: &BTreeMap<String, String>,
62 ) -> Result<Self, SchemaFetchError> {
63 let schema_location = format_options
64 .get(SCHEMA_REGISTRY_KEY)
65 .ok_or_else(|| invalid_option_error!("`{SCHEMA_REGISTRY_KEY}` is required"))?;
66 let client_config = format_options.into();
67 let urls = handle_sr_list(schema_location)?;
68 let client = Client::new(urls, &client_config)?;
69
70 let name_strategy = format_options
71 .get(NAME_STRATEGY_KEY)
72 .map(|s| {
73 name_strategy_from_str(s).ok_or_else(|| {
74 invalid_option_error!("unrecognized schema registry naming strategy: {s}")
75 })
76 })
77 .transpose()?
78 .unwrap_or_default();
79 let key_record_name = format_options.get(KEY_MESSAGE_NAME_KEY).cloned();
80 let val_record_name = format_options.get(MESSAGE_NAME_KEY).cloned();
81
82 Ok(Self {
83 client,
84 name_strategy,
85 topic: topic.into(),
86 key_record_name,
87 val_record_name,
88 })
89 }
90
91 async fn load_schema<Out: LoadedSchema, const IS_KEY: bool>(
92 &self,
93 ) -> Result<(SchemaVersion, Out), SchemaFetchError> {
94 let record = match IS_KEY {
95 true => self.key_record_name.as_deref(),
96 false => self.val_record_name.as_deref(),
97 };
98 let subject = get_subject_by_strategy(&self.name_strategy, &self.topic, record, IS_KEY)?;
99 let (primary_subject, dependency_subjects) =
100 self.client.get_subject_and_references(&subject).await?;
101 let schema_id = primary_subject.schema.id;
102 let out = Out::compile(primary_subject, dependency_subjects)?;
103 Ok((SchemaVersion::Confluent(schema_id), out))
104 }
105}
106
107impl GlueSchemaLoader {
108 pub async fn from_format_options(
109 schema_arn: &str,
110 format_options: &BTreeMap<String, String>,
111 ) -> Result<Self, SchemaFetchError> {
112 risingwave_common::license::Feature::GlueSchemaRegistry.check_available()?;
113 if let Some(mock_config) = format_options.get("aws.glue.mock_config") {
114 let parsed: serde_json::Value =
116 serde_json::from_str(mock_config).expect("mock config shall be valid json");
117 let schema_version_id_str = parsed
118 .get("arn_to_latest_id")
119 .unwrap()
120 .as_object()
121 .unwrap()
122 .get(schema_arn)
123 .unwrap()
124 .as_str()
125 .unwrap();
126 let definition = parsed
127 .get("by_id")
128 .unwrap()
129 .as_object()
130 .unwrap()
131 .get(schema_version_id_str)
132 .unwrap()
133 .to_string();
134 return Ok(Self::Mock {
135 schema_version_id: schema_version_id_str.parse()?,
136 definition,
137 });
138 };
139 let aws_auth_props =
140 serde_json::from_value::<AwsAuthProps>(serde_json::to_value(format_options).unwrap())
141 .map_err(|_e| invalid_option_error!(""))?;
142 let client = aws_sdk_glue::Client::new(
143 &aws_auth_props
144 .build_config()
145 .await
146 .map_err(SchemaFetchError::YetToMigrate)?,
147 );
148 Ok(Self::Real {
149 client,
150 schema_arn: schema_arn.to_owned(),
151 })
152 }
153
154 async fn load_schema<Out: LoadedSchema, const IS_KEY: bool>(
155 &self,
156 ) -> Result<(SchemaVersion, Out), SchemaFetchError> {
157 if IS_KEY {
158 return Err(invalid_option_error!(
159 "GlueSchemaRegistry cannot be key. Specify `KEY ENCODE [TEXT | BYTES]` please."
160 )
161 .into());
162 }
163 let (schema_version_id, definition) = match self {
164 Self::Mock {
165 schema_version_id,
166 definition,
167 } => (*schema_version_id, definition.clone()),
168 Self::Real { client, schema_arn } => {
169 use aws_sdk_glue::types::{SchemaId, SchemaVersionNumber};
170
171 let res = client
172 .get_schema_version()
173 .schema_id(SchemaId::builder().schema_arn(schema_arn).build())
174 .schema_version_number(
175 SchemaVersionNumber::builder().latest_version(true).build(),
176 )
177 .send()
178 .await
179 .map_err(|e| Box::new(e.into_service_error()))?;
180 let schema_version_id = res
181 .schema_version_id()
182 .ok_or_else(|| malformed_response_error!("missing schema_version_id"))?
183 .parse()?;
184 let definition = res
185 .schema_definition()
186 .ok_or_else(|| malformed_response_error!("missing schema_definition"))?
187 .to_owned();
188 (schema_version_id, definition)
189 }
190 };
191
192 let primary = Subject {
195 version: 0,
196 name: "".to_owned(),
197 schema: super::schema_registry::ConfluentSchema {
198 id: 0,
199 content: definition,
200 },
201 };
202 let out = Out::compile(primary, vec![])?;
203 Ok((SchemaVersion::Glue(schema_version_id), out))
204 }
205}
206
207impl SchemaLoader {
208 pub async fn from_format_options(
209 topic: &str,
210 format_options: &BTreeMap<String, String>,
211 ) -> Result<Self, SchemaFetchError> {
212 if let Some(schema_arn) = format_options.get(AWS_GLUE_SCHEMA_ARN_KEY) {
213 Ok(Self::Glue(
214 GlueSchemaLoader::from_format_options(schema_arn, format_options).await?,
215 ))
216 } else {
217 Ok(Self::Confluent(ConfluentSchemaLoader::from_format_options(
218 topic,
219 format_options,
220 )?))
221 }
222 }
223
224 async fn load_schema<Out: LoadedSchema, const IS_KEY: bool>(
225 &self,
226 ) -> Result<(SchemaVersion, Out), SchemaFetchError> {
227 match self {
228 Self::Confluent(inner) => inner.load_schema::<Out, IS_KEY>().await,
229 Self::Glue(inner) => inner.load_schema::<Out, IS_KEY>().await,
230 }
231 }
232
233 pub async fn load_key_schema<Out: LoadedSchema>(
234 &self,
235 ) -> Result<(SchemaVersion, Out), SchemaFetchError> {
236 self.load_schema::<Out, true>().await
237 }
238
239 pub async fn load_val_schema<Out: LoadedSchema>(
240 &self,
241 ) -> Result<(SchemaVersion, Out), SchemaFetchError> {
242 self.load_schema::<Out, false>().await
243 }
244}
245
246pub trait LoadedSchema: Sized {
247 fn compile(primary: Subject, references: Vec<Subject>) -> Result<Self, SchemaFetchError>;
248}