1use std::collections::btree_map::Entry;
39use std::collections::{BTreeMap, BTreeSet};
40use std::fmt;
41use std::sync::Arc;
42
43use anyhow::{Context, anyhow, bail};
44use mz_avro::error::Error as AvroError;
45use mz_avro::schema::{
46 ParseSchemaError, Schema, SchemaNode, SchemaPiece, SchemaPieceOrNamed, resolve_schemas,
47};
48use mz_ore::cast::CastFrom;
49use mz_ore::collections::CollectionExt;
50use mz_ore::future::OreFutureExt;
51use mz_ore::retry::Retry;
52use mz_repr::adt::numeric::{NUMERIC_DATUM_MAX_PRECISION, NumericMaxScale};
53use mz_repr::adt::timestamp::TimestampPrecision;
54use mz_repr::{ColumnName, RelationDesc, SqlColumnType, SqlScalarType, UNKNOWN_COLUMN_NAME};
55use tracing::warn;
56use uuid::Uuid;
57
58use crate::avro::is_null;
59
60pub fn parse_schema(schema: &str, references: &[String]) -> anyhow::Result<Schema> {
61 let schema: serde_json::Value = serde_json::from_str(schema)?;
62 let mut parsed_refs: Vec<Schema> = Vec::with_capacity(references.len());
65 for reference in references {
66 let ref_json: serde_json::Value = serde_json::from_str(reference)?;
67 let parsed = Schema::parse_with_references(&ref_json, &parsed_refs)?;
68 parsed_refs.push(parsed);
69 }
70 Ok(Schema::parse_with_references(&schema, &parsed_refs)?)
71}
72
73pub fn schema_to_relationdesc(schema: Schema) -> Result<RelationDesc, anyhow::Error> {
76 Ok(RelationDesc::from_names_and_types(validate_schema_1(
79 schema.top_node(),
80 )?))
81}
82
83fn validate_schema_1(schema: SchemaNode) -> anyhow::Result<Vec<(ColumnName, SqlColumnType)>> {
86 let mut columns = vec![];
87 let mut seen_avro_nodes = Default::default();
88 match schema.inner {
89 SchemaPiece::Record { fields, .. } => {
90 for f in fields {
91 columns.extend(get_named_columns(
92 &mut seen_avro_nodes,
93 schema.step(&f.schema),
94 Some(&f.name),
95 )?);
96 }
97 }
98 _ => {
99 columns.extend(get_named_columns(&mut seen_avro_nodes, schema, None)?);
100 }
101 }
102 Ok(columns)
103}
104
105fn get_union_columns<'a>(
108 seen_avro_nodes: &mut BTreeSet<usize>,
109 schema: SchemaNode<'a>,
110 base_name: Option<&str>,
111) -> anyhow::Result<Vec<(ColumnName, SqlColumnType)>> {
112 let us = match schema.inner {
113 SchemaPiece::Union(us) => us,
114 _ => panic!("This function should only be called on unions."),
115 };
116 let mut columns = vec![];
117 let vs = us.variants();
118 if vs.is_empty() || (vs.len() == 1 && is_null(&vs[0])) {
119 bail!(anyhow!("Empty or null-only unions are not supported"));
120 } else {
121 for (i, v) in vs.iter().filter(|v| !is_null(v)).enumerate() {
122 with_recursion_guard(seen_avro_nodes, schema.root, v, |seen| {
123 let node = schema.step(v);
124 if let SchemaPiece::Union(_) = node.inner {
125 unreachable!("Internal error: directly nested avro union!");
126 }
127
128 let name = if vs.len() == 1 || (vs.len() == 2 && vs.iter().any(is_null)) {
129 base_name
132 .map(|n| n.to_owned())
133 .or_else(|| {
134 v.get_piece_and_name(schema.root)
135 .1
136 .map(|full_name| full_name.base_name().to_owned())
137 })
138 .unwrap_or_else(|| UNKNOWN_COLUMN_NAME.into())
139 } else {
140 base_name
144 .map(|n| format!("{}{}", n, i + 1))
145 .or_else(|| {
146 v.get_piece_and_name(schema.root)
147 .1
148 .map(|full_name| full_name.base_name().to_owned())
149 })
150 .unwrap_or_else(|| UNKNOWN_COLUMN_NAME.into())
151 };
152
153 let ty = validate_schema_2(seen, node)?;
157 columns.push((name.into(), ty.nullable(vs.len() > 1)));
158 Ok(())
159 })?;
160 }
161 }
162 Ok(columns)
163}
164
165fn get_named_columns<'a>(
166 seen_avro_nodes: &mut BTreeSet<usize>,
167 schema: SchemaNode<'a>,
168 base_name: Option<&str>,
169) -> anyhow::Result<Vec<(ColumnName, SqlColumnType)>> {
170 if let SchemaPiece::Union(_) = schema.inner {
171 get_union_columns(seen_avro_nodes, schema, base_name)
172 } else {
173 let scalar_type = validate_schema_2(seen_avro_nodes, schema)?;
174 Ok(vec![(
175 base_name.unwrap_or(UNKNOWN_COLUMN_NAME).into(),
178 scalar_type.nullable(false),
179 )])
180 }
181}
182
183fn validate_schema_2(
187 seen_avro_nodes: &mut BTreeSet<usize>,
188 schema: SchemaNode,
189) -> anyhow::Result<SqlScalarType> {
190 Ok(match schema.inner {
191 SchemaPiece::Union(_) => {
192 let columns = get_union_columns(seen_avro_nodes, schema, None)?;
193 if columns.len() != 1 {
194 bail!("Union of more than one non-null type not valid here");
195 }
196 let (_column_name, column_type) = columns.into_element();
197 column_type.scalar_type
204 }
205 SchemaPiece::Null => bail!("null outside of union types is not supported"),
206 SchemaPiece::Boolean => SqlScalarType::Bool,
207 SchemaPiece::Int => SqlScalarType::Int32,
208 SchemaPiece::Long => SqlScalarType::Int64,
209 SchemaPiece::Float => SqlScalarType::Float32,
210 SchemaPiece::Double => SqlScalarType::Float64,
211 SchemaPiece::Date => SqlScalarType::Date,
212 SchemaPiece::TimestampMilli => SqlScalarType::Timestamp {
213 precision: Some(TimestampPrecision::try_from(3).unwrap()),
214 },
215 SchemaPiece::TimestampMicro => SqlScalarType::Timestamp {
216 precision: Some(TimestampPrecision::try_from(6).unwrap()),
217 },
218 SchemaPiece::Decimal {
219 precision, scale, ..
220 } => {
221 if *precision > usize::cast_from(NUMERIC_DATUM_MAX_PRECISION) {
222 bail!(
223 "decimals with precision greater than {} are not supported",
224 NUMERIC_DATUM_MAX_PRECISION
225 )
226 }
227 SqlScalarType::Numeric {
228 max_scale: Some(NumericMaxScale::try_from(*scale)?),
229 }
230 }
231 SchemaPiece::Bytes | SchemaPiece::Fixed { .. } => SqlScalarType::Bytes,
232 SchemaPiece::String | SchemaPiece::Enum { .. } => SqlScalarType::String,
233
234 SchemaPiece::Json => SqlScalarType::Jsonb,
235 SchemaPiece::Uuid => SqlScalarType::Uuid,
236 SchemaPiece::Record { fields, .. } => {
237 let mut columns = vec![];
238 for f in fields {
239 with_recursion_guard(seen_avro_nodes, schema.root, &f.schema, |seen| {
240 columns.extend(get_named_columns(
241 seen,
242 schema.step(&f.schema),
243 Some(&f.name),
244 )?);
245 Ok(())
246 })?;
247 }
248 SqlScalarType::Record {
249 fields: columns.into(),
250 custom_id: None,
251 }
252 }
253 SchemaPiece::Array(inner) => {
254 with_recursion_guard(seen_avro_nodes, schema.root, inner.as_ref(), |seen| {
255 Ok(SqlScalarType::List {
256 element_type: Box::new(validate_schema_2(seen, schema.step(inner))?),
257 custom_id: None,
258 })
259 })?
260 }
261 SchemaPiece::Map(inner) => {
262 with_recursion_guard(seen_avro_nodes, schema.root, inner.as_ref(), |seen| {
263 Ok(SqlScalarType::Map {
264 value_type: Box::new(validate_schema_2(seen, schema.step(inner))?),
265 custom_id: None,
266 })
267 })?
268 }
269 _ => bail!("Unsupported type in schema: {:?}", schema.inner),
270 })
271}
272
273fn with_recursion_guard<T>(
277 seen: &mut BTreeSet<usize>,
278 root: &Schema,
279 node: &SchemaPieceOrNamed,
280 f: impl FnOnce(&mut BTreeSet<usize>) -> anyhow::Result<T>,
281) -> anyhow::Result<T> {
282 let named_idx = match node {
283 SchemaPieceOrNamed::Named(idx) => Some(*idx),
284 SchemaPieceOrNamed::Piece(_) => None,
285 };
286 if let Some(named_idx) = named_idx {
287 if !seen.insert(named_idx) {
288 bail!(
289 "Recursive types are not supported: {}",
290 node.get_human_name(root)
291 );
292 }
293 }
294 let result = f(seen);
295 if let Some(named_idx) = named_idx {
296 seen.remove(&named_idx);
297 }
298 result
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 fn assert_recursive(schema: &str) {
310 let err = schema_to_relationdesc(parse_schema(schema, &[]).expect("schema should parse"))
311 .expect_err("recursive schema should be rejected");
312 assert!(
313 err.to_string()
314 .contains("Recursive types are not supported"),
315 "unexpected error: {err}"
316 );
317 }
318
319 #[mz_ore::test]
320 fn recursive_record_field() {
321 assert_recursive(r#"{"type":"record","name":"a","fields":[{"name":"f","type":"a"}]}"#);
322 }
323
324 #[mz_ore::test]
325 fn recursive_union() {
326 assert_recursive(
327 r#"{"type":"record","name":"a","fields":[{"name":"f","type":["a","null"]}]}"#,
328 );
329 }
330
331 #[mz_ore::test]
332 fn recursive_array() {
333 assert_recursive(
334 r#"{"type":"record","name":"a","fields":[{"name":"f","type":{"type":"array","items":"a"}}]}"#,
335 );
336 }
337
338 #[mz_ore::test]
339 fn recursive_map() {
340 assert_recursive(
341 r#"{"type":"record","name":"a","fields":[{"name":"f","type":{"type":"map","values":"a"}}]}"#,
342 );
343 }
344
345 #[mz_ore::test]
349 fn repeated_named_type_is_not_recursive() {
350 let schema = r#"{
351 "type": "record",
352 "name": "outer",
353 "fields": [
354 {"name": "a", "type": {"type": "record", "name": "inner", "fields": [{"name": "x", "type": "int"}]}},
355 {"name": "b", "type": "inner"}
356 ]
357 }"#;
358 let desc = schema_to_relationdesc(parse_schema(schema, &[]).expect("schema should parse"))
359 .expect("diamond reuse of a named type should be allowed");
360 assert_eq!(desc.arity(), 2);
361 }
362
363 #[mz_ore::test]
364 fn registry_name_from_schema_arn_commercial() {
365 assert_eq!(
366 registry_name_from_schema_arn(
367 "arn:aws:glue:us-east-1:123456789012:schema/myreg/myschema"
368 ),
369 Some("myreg")
370 );
371 }
372
373 #[mz_ore::test]
374 fn registry_name_from_schema_arn_china_partition() {
375 assert_eq!(
376 registry_name_from_schema_arn(
377 "arn:aws-cn:glue:cn-north-1:123456789012:schema/myreg/myschema"
378 ),
379 Some("myreg")
380 );
381 }
382
383 #[mz_ore::test]
384 fn registry_name_from_schema_arn_govcloud_partition() {
385 assert_eq!(
386 registry_name_from_schema_arn(
387 "arn:aws-us-gov:glue:us-gov-west-1:123456789012:schema/myreg/myschema"
388 ),
389 Some("myreg")
390 );
391 }
392
393 #[mz_ore::test]
394 fn registry_name_from_schema_arn_rejects_non_aws_partition() {
395 assert_eq!(
396 registry_name_from_schema_arn(
397 "arn:gcp:glue:us-east-1:123456789012:schema/myreg/myschema"
398 ),
399 None
400 );
401 }
402
403 #[mz_ore::test]
404 fn registry_name_from_schema_arn_rejects_missing_arn_prefix() {
405 assert_eq!(registry_name_from_schema_arn("schema/myreg/myschema"), None);
408 }
409
410 #[mz_ore::test]
411 fn registry_name_from_schema_arn_rejects_non_glue_service() {
412 assert_eq!(
413 registry_name_from_schema_arn(
414 "arn:aws:s3:us-east-1:123456789012:schema/myreg/myschema"
415 ),
416 None
417 );
418 }
419
420 #[mz_ore::test]
421 fn registry_name_from_schema_arn_rejects_missing_schema_segment() {
422 assert_eq!(
423 registry_name_from_schema_arn("arn:aws:glue:us-east-1:123456789012:table/myreg/foo"),
424 None
425 );
426 }
427
428 #[mz_ore::test]
429 fn registry_name_from_schema_arn_rejects_empty_registry() {
430 assert_eq!(
431 registry_name_from_schema_arn("arn:aws:glue:us-east-1:123456789012:schema//myschema"),
432 None
433 );
434 }
435
436 #[mz_ore::test]
437 fn registry_name_from_schema_arn_allows_schema_name_with_slashes() {
438 assert_eq!(
441 registry_name_from_schema_arn(
442 "arn:aws:glue:us-east-1:123456789012:schema/myreg/path/like/name"
443 ),
444 Some("myreg")
445 );
446 }
447
448 const GLUE_SCHEMA_ARN: &str = "arn:aws:glue:us-east-1:123456789012:schema/myreg/myschema";
453 const GLUE_SCHEMA_BODY: &str =
454 r#"{"type": "record", "name": "row", "fields": [{"name": "a", "type": "long"}]}"#;
455
456 fn glue_reader_schema() -> Schema {
457 parse_schema(GLUE_SCHEMA_BODY, &[]).expect("reader schema parses")
458 }
459
460 fn glue_schema_version() -> mz_aws_glue_schema_registry::SchemaVersion {
463 mz_aws_glue_schema_registry::SchemaVersion {
464 schema_version_id: Some("00000000-0000-0000-0000-000000000001".to_string()),
465 schema_arn: Some(GLUE_SCHEMA_ARN.to_string()),
466 definition: Some(GLUE_SCHEMA_BODY.to_string()),
467 data_format: Some(mz_aws_glue_schema_registry::DataFormat::Avro),
468 version_number: Some(1),
469 lifecycle_status: Some(
470 mz_aws_glue_schema_registry::SchemaVersionLifecycleStatus::Available,
471 ),
472 }
473 }
474
475 fn parse_glue_version(
476 version: mz_aws_glue_schema_registry::SchemaVersion,
477 ) -> Result<Schema, AvroError> {
478 GlueSchemaCache::parse_version(version, Uuid::nil(), "myreg", &glue_reader_schema())
479 }
480
481 #[mz_ore::test]
482 fn parse_version_accepts_matching_available_schema() {
483 parse_glue_version(glue_schema_version())
484 .expect("an Available schema in the expected registry should parse");
485 }
486
487 #[mz_ore::test]
488 fn parse_version_rejects_non_available_lifecycle() {
489 let mut version = glue_schema_version();
490 version.lifecycle_status =
491 Some(mz_aws_glue_schema_registry::SchemaVersionLifecycleStatus::Pending);
492 let err = parse_glue_version(version).expect_err("a non-Available version is refused");
493 assert!(err.to_string().contains("is not Available"), "got: {err}");
494 }
495
496 #[mz_ore::test]
497 fn parse_version_rejects_missing_definition() {
498 let mut version = glue_schema_version();
499 version.definition = None;
500 let err =
501 parse_glue_version(version).expect_err("a version without a definition is refused");
502 assert!(
503 err.to_string().contains("returned without a definition"),
504 "got: {err}"
505 );
506 }
507
508 #[mz_ore::test]
509 fn parse_version_rejects_missing_arn() {
510 let mut version = glue_schema_version();
511 version.schema_arn = None;
512 let err = parse_glue_version(version).expect_err("a version without an ARN is refused");
513 assert!(
514 err.to_string().contains("returned without a SchemaArn"),
515 "got: {err}"
516 );
517 }
518
519 #[mz_ore::test]
520 fn parse_version_rejects_unparseable_arn() {
521 let mut version = glue_schema_version();
522 version.schema_arn = Some("not-an-arn".to_string());
523 let err = parse_glue_version(version).expect_err("a malformed ARN is refused");
524 assert!(
525 err.to_string().contains("did not match the expected"),
526 "got: {err}"
527 );
528 }
529
530 #[mz_ore::test]
531 fn parse_version_rejects_registry_mismatch() {
532 let mut version = glue_schema_version();
535 version.schema_arn =
536 Some("arn:aws:glue:us-east-1:123456789012:schema/otherreg/myschema".to_string());
537 let err = parse_glue_version(version).expect_err("a foreign-registry version is refused");
538 let msg = err.to_string();
539 assert!(msg.contains("lives in registry \"otherreg\""), "got: {msg}");
540 assert!(
541 msg.contains("configured for registry \"myreg\""),
542 "got: {msg}"
543 );
544 }
545
546 #[mz_ore::test]
547 fn parse_version_rejects_invalid_json_definition() {
548 let mut version = glue_schema_version();
549 version.definition = Some("{ this is not json".to_string());
550 let err = parse_glue_version(version).expect_err("an unparseable definition is refused");
551 assert!(err.to_string().contains("Error parsing JSON"), "got: {err}");
552 }
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum WriterSchemaKey {
562 Confluent(i32),
563 Glue(Uuid),
564}
565
566impl fmt::Display for WriterSchemaKey {
567 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
568 match self {
569 WriterSchemaKey::Confluent(id) => write!(f, "Confluent schema id {}", id),
570 WriterSchemaKey::Glue(uuid) => write!(f, "Glue schema-version {}", uuid),
571 }
572 }
573}
574
575pub enum WriterSchemaProvider {
584 None,
587 Confluent { cache: Option<SchemaCache> },
591 Glue { cache: Option<GlueSchemaCache> },
594}
595
596impl fmt::Debug for WriterSchemaProvider {
597 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598 let tag = match self {
599 WriterSchemaProvider::None => "none",
600 WriterSchemaProvider::Confluent { cache: None } => "confluent (no cache)",
601 WriterSchemaProvider::Confluent { cache: Some(_) } => "confluent",
602 WriterSchemaProvider::Glue { cache: None } => "glue (no cache)",
603 WriterSchemaProvider::Glue { cache: Some(_) } => "glue",
604 };
605 f.debug_tuple("WriterSchemaProvider").field(&tag).finish()
606 }
607}
608
609impl WriterSchemaProvider {
610 pub fn confluent(ccsr_client: Option<mz_ccsr::Client>) -> Self {
614 let cache = ccsr_client.map(SchemaCache::new);
615 WriterSchemaProvider::Confluent { cache }
616 }
617
618 pub fn glue(
628 client_with_registry: Option<(mz_aws_glue_schema_registry::Client, String)>,
629 ) -> Self {
630 let cache = client_with_registry.map(|(c, registry)| GlueSchemaCache::new(c, registry));
631 WriterSchemaProvider::Glue { cache }
632 }
633}
634
635pub struct AvroSchemaResolver {
636 reader_schema: Schema,
637 writer_schemas: WriterSchemaProvider,
638}
639
640impl AvroSchemaResolver {
641 pub fn new(
642 reader_schema: &str,
643 reader_reference_schemas: &[String],
644 writer_schemas: WriterSchemaProvider,
645 ) -> anyhow::Result<Self> {
646 let reader_schema = parse_schema(reader_schema, reader_reference_schemas)?;
648 Ok(Self {
649 reader_schema,
650 writer_schemas,
651 })
652 }
653
654 pub async fn resolve<'a, 'b>(
655 &'a mut self,
656 mut bytes: &'b [u8],
657 ) -> anyhow::Result<anyhow::Result<(&'b [u8], &'a Schema, Option<WriterSchemaKey>)>> {
658 let (resolved_schema, key) = match &mut self.writer_schemas {
659 WriterSchemaProvider::None => (&self.reader_schema, None),
660
661 WriterSchemaProvider::Confluent { cache: None } => {
662 match crate::confluent::extract_avro_header(bytes) {
666 Ok((_id, adjusted_bytes)) => {
667 bytes = adjusted_bytes;
668 (&self.reader_schema, None)
669 }
670 Err(err) => return Ok(Err(err)),
671 }
672 }
673
674 WriterSchemaProvider::Confluent { cache: Some(cache) } => {
675 let (id, adjusted_bytes) = match crate::confluent::extract_avro_header(bytes) {
676 Ok(ok) => ok,
677 Err(err) => return Ok(Err(err)),
678 };
679 bytes = adjusted_bytes;
680 let result = cache
681 .get(id, &self.reader_schema)
682 .await?
687 .with_context(|| format!("failed to resolve Avro schema (id = {id})"));
688 let schema = match result {
689 Ok(schema) => schema,
690 Err(err) => return Ok(Err(err)),
691 };
692 (schema, Some(WriterSchemaKey::Confluent(id)))
693 }
694
695 WriterSchemaProvider::Glue { cache: None } => {
696 match crate::glue::extract_avro_header(bytes) {
698 Ok((_uuid, adjusted_bytes)) => {
699 bytes = adjusted_bytes;
700 (&self.reader_schema, None)
701 }
702 Err(err) => return Ok(Err(err)),
703 }
704 }
705
706 WriterSchemaProvider::Glue { cache: Some(cache) } => {
707 let (uuid, adjusted_bytes) = match crate::glue::extract_avro_header(bytes) {
708 Ok(ok) => ok,
709 Err(err) => return Ok(Err(err)),
710 };
711 bytes = adjusted_bytes;
712 let result = cache
713 .get(uuid, &self.reader_schema)
714 .await?
715 .with_context(|| format!("failed to resolve Avro schema (uuid = {uuid})"));
716 let schema = match result {
717 Ok(schema) => schema,
718 Err(err) => return Ok(Err(err)),
719 };
720 (schema, Some(WriterSchemaKey::Glue(uuid)))
721 }
722 };
723 Ok(Ok((bytes, resolved_schema, key)))
724 }
725}
726
727impl fmt::Debug for AvroSchemaResolver {
728 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
729 f.debug_struct("AvroSchemaResolver")
730 .field("reader_schema", &self.reader_schema)
731 .field("writer_schemas", &self.writer_schemas)
732 .finish()
733 }
734}
735
736#[derive(Debug)]
746pub struct GlueSchemaCache {
747 cache: BTreeMap<Uuid, Result<Schema, AvroError>>,
748 glue_client: mz_aws_glue_schema_registry::Client,
749 expected_registry: String,
753}
754
755impl GlueSchemaCache {
756 fn new(glue_client: mz_aws_glue_schema_registry::Client, expected_registry: String) -> Self {
757 GlueSchemaCache {
758 cache: BTreeMap::new(),
759 glue_client,
760 expected_registry,
761 }
762 }
763
764 async fn get(
773 &mut self,
774 uuid: Uuid,
775 reader_schema: &Schema,
776 ) -> anyhow::Result<anyhow::Result<&Schema>> {
777 let entry = match self.cache.entry(uuid) {
778 Entry::Occupied(o) => o.into_mut(),
779 Entry::Vacant(v) => {
780 let parsed: Result<Schema, AvroError> = match self
781 .glue_client
782 .get_schema_version_by_id(uuid)
783 .await
784 {
785 Ok(version) => {
786 Self::parse_version(version, uuid, &self.expected_registry, reader_schema)
787 }
788 Err(mz_aws_glue_schema_registry::GetSchemaVersionError::NotFound) => Err(
792 ParseSchemaError::new(format!("Glue schema version {uuid} not found"))
793 .into(),
794 ),
795 Err(e @ mz_aws_glue_schema_registry::GetSchemaVersionError::Other(_)) => {
801 return Err(e.into());
802 }
803 };
804 v.insert(parsed)
805 }
806 };
807 Ok(entry.as_ref().map_err(|e| anyhow::Error::new(e.clone())))
808 }
809
810 fn parse_version(
815 version: mz_aws_glue_schema_registry::SchemaVersion,
816 uuid: Uuid,
817 expected_registry: &str,
818 reader_schema: &Schema,
819 ) -> Result<Schema, AvroError> {
820 use mz_aws_glue_schema_registry::SchemaVersionLifecycleStatus;
821
822 if !matches!(
826 version.lifecycle_status,
827 Some(SchemaVersionLifecycleStatus::Available)
828 ) {
829 return Err(ParseSchemaError::new(format!(
830 "Glue schema version {uuid} is not Available (status: {:?}); \
831 refusing to decode",
832 version.lifecycle_status
833 ))
834 .into());
835 }
836 let definition = version.definition.ok_or_else(|| {
837 ParseSchemaError::new(format!(
838 "Glue schema version {uuid} returned without a definition"
839 ))
840 })?;
841 let arn = version.schema_arn.as_deref().ok_or_else(|| {
846 ParseSchemaError::new(format!(
847 "Glue schema version {uuid} returned without a SchemaArn; \
848 cannot verify registry membership"
849 ))
850 })?;
851 let actual_registry = registry_name_from_schema_arn(arn).ok_or_else(|| {
852 ParseSchemaError::new(format!(
853 "Glue SchemaArn {arn:?} did not match the expected \
854 arn:aws[-...]:glue:<region>:<account>:schema/<registry>/<schema> form"
855 ))
856 })?;
857 if actual_registry != expected_registry {
858 return Err(ParseSchemaError::new(format!(
859 "Glue schema version {uuid} lives in registry {actual_registry:?} \
860 but this source is configured for registry {expected:?}; \
861 refusing to decode",
862 expected = expected_registry,
863 ))
864 .into());
865 }
866 let value: serde_json::Value = serde_json::from_str(&definition)
867 .map_err(|e| ParseSchemaError::new(format!("Error parsing JSON: {e}")))?;
868 let schema = Schema::parse_with_references(&value, &[])?;
869 resolve_schemas(&schema, reader_schema)
870 }
871}
872
873fn registry_name_from_schema_arn(arn: &str) -> Option<&str> {
884 let rest = arn.strip_prefix("arn:")?;
885 let (partition, after_partition) = rest.split_once(":glue:")?;
886 if !partition.starts_with("aws") {
887 return None;
888 }
889 let (_, after_schema) = after_partition.split_once(":schema/")?;
890 let (registry, _) = after_schema.split_once('/')?;
891 if registry.is_empty() {
892 return None;
893 }
894 Some(registry)
895}
896
897#[derive(Debug)]
902pub struct SchemaCache {
903 cache: BTreeMap<i32, Result<Schema, AvroError>>,
904 ccsr_client: Arc<mz_ccsr::Client>,
905}
906
907impl SchemaCache {
908 fn new(ccsr_client: mz_ccsr::Client) -> SchemaCache {
909 SchemaCache {
910 cache: BTreeMap::new(),
911 ccsr_client: Arc::new(ccsr_client),
912 }
913 }
914
915 async fn get(
925 &mut self,
926 id: i32,
927 reader_schema: &Schema,
928 ) -> anyhow::Result<anyhow::Result<&Schema>> {
929 let entry = match self.cache.entry(id) {
930 Entry::Occupied(o) => o.into_mut(),
931 Entry::Vacant(v) => {
932 let ccsr_client = Arc::clone(&self.ccsr_client);
936
937 let (primary_subject, reference_subjects) = Retry::default()
939 .max_duration(ccsr_client.timeout() * 2)
941 .retry_async_canceling(move |state| {
943 let ccsr_client = Arc::clone(&ccsr_client);
944 async move {
945 let res = ccsr_client.get_subject_and_references_by_id(id).await;
946 match res {
947 Err(e) => {
948 if let Some(timeout) = state.next_backoff {
949 warn!(
950 "transient failure fetching \
951 schema id {}: {:?}, retrying in {:?}",
952 id, e, timeout
953 );
954 }
955 Err(anyhow::Error::from(e))
956 }
957 _ => Ok(res?),
958 }
959 }
960 })
961 .run_in_task(|| format!("fetch_avro_schema:{}", id))
962 .await?;
963
964 let result = Self::parse_with_references(
971 &primary_subject,
972 &reference_subjects,
973 reader_schema,
974 );
975 v.insert(result)
976 }
977 };
978 Ok(entry.as_ref().map_err(|e| anyhow::Error::new(e.clone())))
979 }
980
981 fn parse_with_references(
983 primary_subject: &mz_ccsr::Subject,
984 reference_subjects: &[mz_ccsr::Subject],
985 reader_schema: &Schema,
986 ) -> Result<Schema, AvroError> {
987 let mut reference_schemas: Vec<Schema> = Vec::with_capacity(reference_subjects.len());
989 for subject in reference_subjects {
990 let ref_json: serde_json::Value = serde_json::from_str(&subject.schema.raw)
991 .map_err(|e| ParseSchemaError::new(format!("Error parsing JSON: {}", e)))?;
992 let parsed = Schema::parse_with_references(&ref_json, &reference_schemas)?;
993 reference_schemas.push(parsed);
994 }
995
996 let primary_value: serde_json::Value = serde_json::from_str(&primary_subject.schema.raw)
998 .map_err(|e| ParseSchemaError::new(format!("Error parsing JSON: {}", e)))?;
999 let schema = Schema::parse_with_references(&primary_value, &reference_schemas)?;
1000
1001 let resolved = resolve_schemas(&schema, reader_schema)?;
1004 Ok(resolved)
1005 }
1006}