1use std::collections::BTreeMap;
11use std::fmt;
12use std::sync::LazyLock;
13
14use anyhow::Ok;
15use byteorder::{NetworkEndian, WriteBytesExt};
16use chrono::Timelike;
17use itertools::Itertools;
18use mz_avro::Schema;
19use mz_avro::types::{DecimalValue, ToAvro, Value};
20use mz_ore::cast::CastFrom;
21use mz_repr::adt::jsonb::JsonbRef;
22use mz_repr::adt::numeric::{self, NUMERIC_AGG_MAX_PRECISION, NUMERIC_DATUM_MAX_PRECISION};
23use mz_repr::{CatalogItemId, ColumnName, Datum, RelationDesc, Row, SqlColumnType, SqlScalarType};
24use serde_json::json;
25use uuid::Uuid;
26
27use crate::encode::{Encode, TypedDatum, column_names_and_types};
28use crate::envelopes::{self, DBZ_ROW_TYPE_ID, ENVELOPE_CUSTOM_NAMES};
29use crate::json::{SchemaOptions, build_row_schema_json};
30
31static DEBEZIUM_TRANSACTION_SCHEMA: LazyLock<Schema> = LazyLock::new(|| {
38 Schema::parse(&json!({
39 "type": "record",
40 "name": "envelope",
41 "fields": [
42 {
43 "name": "id",
44 "type": "string"
45 },
46 {
47 "name": "status",
48 "type": "string"
49 },
50 {
51 "name": "event_count",
52 "type": [
53 "null",
54 "long"
55 ]
56 },
57 {
58 "name": "data_collections",
59 "type": [
60 "null",
61 {
62 "type": "array",
63 "items": {
64 "type": "record",
65 "name": "data_collection",
66 "fields": [
67 {
68 "name": "data_collection",
69 "type": "string"
70 },
71 {
72 "name": "event_count",
73 "type": "long"
74 },
75 ]
76 }
77 }
78 ],
79 "default": null,
80 },
81 ]
82 }))
83 .expect("valid schema constructed")
84});
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AvroSchemaId {
95 Confluent(i32),
97 Glue(Uuid),
100}
101
102fn encode_confluent_header(buf: &mut Vec<u8>, schema_id: i32) {
103 buf.write_u8(0).expect("writing to vec cannot fail");
109 buf.write_i32::<NetworkEndian>(schema_id)
110 .expect("writing to vec cannot fail");
111}
112
113fn encode_message_unchecked(
114 schema_id: AvroSchemaId,
115 row: Row,
116 schema: &Schema,
117 columns: &[(ColumnName, SqlColumnType)],
118) -> Vec<u8> {
119 let value = encode_datums_as_avro(row.iter(), columns);
120 match schema_id {
121 AvroSchemaId::Confluent(id) => {
122 let mut buf = vec![];
123 encode_confluent_header(&mut buf, id);
124 mz_avro::encode_unchecked(&value, schema, &mut buf);
125 buf
126 }
127 AvroSchemaId::Glue(id) => {
128 let mut buf = vec![];
129 crate::glue::write_avro_header(&mut buf, id);
130 mz_avro::encode_unchecked(&value, schema, &mut buf);
131 buf
132 }
133 }
134}
135
136#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
137pub enum DocTarget {
138 Type(CatalogItemId),
139 Field {
140 object_id: CatalogItemId,
141 column_name: ColumnName,
142 },
143}
144
145impl DocTarget {
146 fn id(&self) -> CatalogItemId {
147 match self {
148 DocTarget::Type(object_id) => *object_id,
149 DocTarget::Field { object_id, .. } => *object_id,
150 }
151 }
152}
153
154pub struct AvroSchemaGenerator {
156 columns: Vec<(ColumnName, SqlColumnType)>,
157 schema: Schema,
158}
159
160impl fmt::Debug for AvroSchemaGenerator {
161 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
162 f.debug_struct("SchemaGenerator")
163 .field("writer_schema", &self.schema())
164 .finish()
165 }
166}
167
168impl AvroSchemaGenerator {
169 pub fn new(
170 desc: RelationDesc,
171 debezium: bool,
172 mut doc_options: BTreeMap<DocTarget, String>,
173 avro_fullname: &str,
174 set_null_defaults: bool,
175 sink_from: Option<CatalogItemId>,
176 use_custom_envelope_names: bool,
177 ) -> Result<Self, anyhow::Error> {
178 let mut columns = column_names_and_types(desc);
179 if debezium {
180 columns = envelopes::dbz_envelope(columns);
181 if let Some(sink_from_id) = sink_from {
185 let mut new_column_docs = BTreeMap::new();
186 doc_options.iter().for_each(|(k, v)| {
187 if k.id() == sink_from_id {
188 match k {
189 DocTarget::Field { column_name, .. } => {
190 new_column_docs.insert(
191 DocTarget::Field {
192 object_id: DBZ_ROW_TYPE_ID,
193 column_name: column_name.clone(),
194 },
195 v.clone(),
196 );
197 }
198 DocTarget::Type(_) => {
199 new_column_docs.insert(DocTarget::Type(DBZ_ROW_TYPE_ID), v.clone());
200 }
201 }
202 }
203 });
204 doc_options.append(&mut new_column_docs);
205 doc_options.retain(|k, _v| k.id() != sink_from_id);
206 }
207 }
208 let custom_names = if use_custom_envelope_names {
209 &ENVELOPE_CUSTOM_NAMES
210 } else {
211 &BTreeMap::new()
212 };
213 let row_schema = build_row_schema_json(
214 &columns,
215 avro_fullname,
216 custom_names,
217 sink_from,
218 &SchemaOptions {
219 set_null_defaults,
220 doc_comments: doc_options,
221 },
222 )?;
223 let schema = Schema::parse(&row_schema).expect("valid schema constructed");
224 Ok(AvroSchemaGenerator { columns, schema })
225 }
226
227 pub fn schema(&self) -> &Schema {
228 &self.schema
229 }
230
231 pub fn columns(&self) -> &[(ColumnName, SqlColumnType)] {
232 &self.columns
233 }
234}
235
236pub struct AvroEncoder {
238 columns: Vec<(ColumnName, SqlColumnType)>,
239 schema: Schema,
240 schema_id: AvroSchemaId,
241}
242
243impl fmt::Debug for AvroEncoder {
244 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245 f.debug_struct("AvroEncoder")
246 .field("writer_schema", &self.schema)
247 .finish()
248 }
249}
250
251impl AvroEncoder {
252 pub fn new(desc: RelationDesc, debezium: bool, schema: &str, schema_id: AvroSchemaId) -> Self {
253 let mut columns = column_names_and_types(desc);
254 if debezium {
255 columns = envelopes::dbz_envelope(columns);
256 };
257 AvroEncoder {
258 columns,
259 schema: Schema::parse(&serde_json::from_str(schema).expect("valid schema json"))
260 .expect("valid schema"),
261 schema_id,
262 }
263 }
264}
265
266impl Encode for AvroEncoder {
267 fn encode_unchecked(&self, row: Row) -> Vec<u8> {
268 encode_message_unchecked(self.schema_id, row, &self.schema, &self.columns)
269 }
270
271 fn hash(&self, buf: &[u8]) -> u64 {
272 let payload = match self.schema_id {
277 AvroSchemaId::Confluent(_) => {
278 crate::confluent::extract_avro_header(buf)
279 .expect("encode_unchecked wrote a Confluent header")
280 .1
281 }
282 AvroSchemaId::Glue(_) => {
283 crate::glue::extract_avro_header(buf)
284 .expect("encode_unchecked wrote a Glue header")
285 .1
286 }
287 };
288 seahash::hash(payload)
289 }
290}
291
292pub fn encode_datums_as_avro<'a, I>(datums: I, names_types: &[(ColumnName, SqlColumnType)]) -> Value
294where
295 I: IntoIterator<Item = Datum<'a>>,
296{
297 let value_fields: Vec<(String, Value)> = names_types
298 .iter()
299 .zip_eq(datums)
300 .map(|((name, typ), datum)| {
301 let name = name.as_str().to_owned();
302 (name, TypedDatum::new(datum, typ).avro())
303 })
304 .collect();
305 let v = Value::Record(value_fields);
306 v
307}
308
309impl<'a> mz_avro::types::ToAvro for TypedDatum<'a> {
310 fn avro(self) -> Value {
311 let TypedDatum { datum, typ } = self;
312 if typ.nullable && datum.is_null() {
313 Value::Union {
314 index: 0,
315 inner: Box::new(Value::Null),
316 n_variants: 2,
317 null_variant: Some(0),
318 }
319 } else {
320 let mut val = match &typ.scalar_type {
321 SqlScalarType::AclItem => Value::String(datum.unwrap_acl_item().to_string()),
322 SqlScalarType::Bool => Value::Boolean(datum.unwrap_bool()),
323 SqlScalarType::PgLegacyChar => {
324 Value::Fixed(1, datum.unwrap_uint8().to_le_bytes().into())
325 }
326 SqlScalarType::Int16 => Value::Int(i32::from(datum.unwrap_int16())),
327 SqlScalarType::Int32 => Value::Int(datum.unwrap_int32()),
328 SqlScalarType::Int64 => Value::Long(datum.unwrap_int64()),
329 SqlScalarType::UInt16 => {
330 Value::Fixed(2, datum.unwrap_uint16().to_be_bytes().into())
331 }
332 SqlScalarType::UInt32 => {
333 Value::Fixed(4, datum.unwrap_uint32().to_be_bytes().into())
334 }
335 SqlScalarType::UInt64 => {
336 Value::Fixed(8, datum.unwrap_uint64().to_be_bytes().into())
337 }
338 SqlScalarType::Oid
339 | SqlScalarType::RegClass
340 | SqlScalarType::RegProc
341 | SqlScalarType::RegType => {
342 Value::Fixed(4, datum.unwrap_uint32().to_be_bytes().into())
343 }
344 SqlScalarType::Float32 => Value::Float(datum.unwrap_float32()),
345 SqlScalarType::Float64 => Value::Double(datum.unwrap_float64()),
346 SqlScalarType::Numeric { max_scale } => {
347 let mut d = datum.unwrap_numeric().0;
348 let (unscaled, precision, scale) = match max_scale {
349 Some(max_scale) => {
350 numeric::rescale(&mut d, max_scale.into_u8()).unwrap();
352 (
353 numeric::numeric_to_twos_complement_be(d).to_vec(),
354 NUMERIC_DATUM_MAX_PRECISION,
355 max_scale.into_u8(),
356 )
357 }
358 None => (
363 numeric::numeric_to_twos_complement_wide(d).to_vec(),
364 NUMERIC_AGG_MAX_PRECISION,
365 NUMERIC_DATUM_MAX_PRECISION,
366 ),
367 };
368 Value::Decimal(DecimalValue {
369 unscaled,
370 precision: usize::cast_from(precision),
371 scale: usize::cast_from(scale),
372 })
373 }
374 SqlScalarType::Date => Value::Date(datum.unwrap_date().unix_epoch_days()),
375 SqlScalarType::Time => Value::Long({
376 let time = datum.unwrap_time();
377 i64::from(time.num_seconds_from_midnight()) * 1_000_000
378 + i64::from(time.nanosecond()) / 1_000
379 }),
380 SqlScalarType::Timestamp { .. } => {
381 Value::Timestamp(datum.unwrap_timestamp().to_naive())
382 }
383 SqlScalarType::TimestampTz { .. } => {
384 Value::Timestamp(datum.unwrap_timestamptz().to_naive())
385 }
386 SqlScalarType::Interval => Value::Fixed(16, {
390 let iv = datum.unwrap_interval();
391 let mut buf = Vec::with_capacity(16);
392 buf.extend(iv.months.to_le_bytes());
393 buf.extend(iv.days.to_le_bytes());
394 buf.extend(iv.micros.to_le_bytes());
395 debug_assert_eq!(buf.len(), 16);
396 buf
397 }),
398 SqlScalarType::Bytes => Value::Bytes(Vec::from(datum.unwrap_bytes())),
399 SqlScalarType::String
400 | SqlScalarType::VarChar { .. }
401 | SqlScalarType::PgLegacyName => Value::String(datum.unwrap_str().to_owned()),
402 SqlScalarType::Char { length } => {
403 let s = mz_repr::adt::char::format_str_pad(datum.unwrap_str(), *length);
404 Value::String(s)
405 }
406 SqlScalarType::Jsonb => Value::Json(JsonbRef::from_datum(datum).to_serde_json()),
407 SqlScalarType::Uuid => Value::Uuid(datum.unwrap_uuid()),
408 ty @ (SqlScalarType::Array(..)
409 | SqlScalarType::Int2Vector
410 | SqlScalarType::List { .. }) => {
411 let list = match ty {
412 SqlScalarType::Array(_) | SqlScalarType::Int2Vector => {
413 datum.unwrap_array().elements()
414 }
415 SqlScalarType::List { .. } => datum.unwrap_list(),
416 _ => unreachable!(),
417 };
418
419 let values = list
420 .into_iter()
421 .map(|datum| {
422 TypedDatum::new(
423 datum,
424 &SqlColumnType {
425 nullable: true,
426 scalar_type: ty.unwrap_collection_element_type().clone(),
427 },
428 )
429 .avro()
430 })
431 .collect();
432 Value::Array(values)
433 }
434 SqlScalarType::Map { value_type, .. } => {
435 let map = datum.unwrap_map();
436 let elements = map
437 .into_iter()
438 .map(|(key, datum)| {
439 let value = TypedDatum::new(
440 datum,
441 &SqlColumnType {
442 nullable: true,
443 scalar_type: (**value_type).clone(),
444 },
445 )
446 .avro();
447 (key.to_string(), value)
448 })
449 .collect();
450 Value::Map(elements)
451 }
452 SqlScalarType::Record { fields, .. } => {
453 let list = datum.unwrap_list();
454 let fields = fields
455 .iter()
456 .zip_eq(list)
457 .map(|((name, typ), datum)| {
458 let name = name.to_string();
459 let datum = TypedDatum::new(datum, typ);
460 let value = datum.avro();
461 (name, value)
462 })
463 .collect();
464 Value::Record(fields)
465 }
466 SqlScalarType::MzTimestamp => {
467 Value::String(datum.unwrap_mz_timestamp().to_string())
468 }
469 SqlScalarType::Range { .. } => Value::String(datum.unwrap_range().to_string()),
470 SqlScalarType::MzAclItem => Value::String(datum.unwrap_mz_acl_item().to_string()),
471 };
472 if typ.nullable {
473 val = Value::Union {
474 index: 1,
475 inner: Box::new(val),
476 n_variants: 2,
477 null_variant: Some(0),
478 };
479 }
480 val
481 }
482 }
483}
484
485pub fn get_debezium_transaction_schema() -> &'static Schema {
486 &DEBEZIUM_TRANSACTION_SCHEMA
487}
488
489pub fn encode_debezium_transaction_unchecked(
490 schema_id: i32,
491 collection: &str,
492 id: &str,
493 status: &str,
494 message_count: Option<i64>,
495) -> Vec<u8> {
496 let mut buf = Vec::new();
497 encode_confluent_header(&mut buf, schema_id);
498
499 let transaction_id = Value::String(id.to_owned());
500 let status = Value::String(status.to_owned());
501 let event_count = match message_count {
502 None => Value::Union {
503 index: 0,
504 inner: Box::new(Value::Null),
505 n_variants: 2,
506 null_variant: Some(0),
507 },
508 Some(count) => Value::Union {
509 index: 1,
510 inner: Box::new(Value::Long(count)),
511 n_variants: 2,
512 null_variant: Some(0),
513 },
514 };
515
516 let data_collections = if let Some(message_count) = message_count {
517 let collection = Value::Record(vec![
518 ("data_collection".into(), Value::String(collection.into())),
519 ("event_count".into(), Value::Long(message_count)),
520 ]);
521 Value::Union {
522 index: 1,
523 inner: Box::new(Value::Array(vec![collection])),
524 n_variants: 2,
525 null_variant: Some(0),
526 }
527 } else {
528 Value::Union {
529 index: 0,
530 inner: Box::new(Value::Null),
531 n_variants: 2,
532 null_variant: Some(0),
533 }
534 };
535
536 let record_contents = vec![
537 ("id".into(), transaction_id),
538 ("status".into(), status),
539 ("event_count".into(), event_count),
540 ("data_collections".into(), data_collections),
541 ];
542 let avro = Value::Record(record_contents);
543 debug_assert!(avro.validate(DEBEZIUM_TRANSACTION_SCHEMA.top_node()));
544 mz_avro::encode_unchecked(&avro, &DEBEZIUM_TRANSACTION_SCHEMA, &mut buf);
545 buf
546}
547
548#[cfg(test)]
549mod tests {
550 use mz_repr::{Datum, RelationDesc, Row, SqlScalarType};
551 use uuid::Uuid;
552
553 use super::*;
554 use crate::encode::Encode;
555
556 const SCHEMA: &str = r#"{"type":"record","name":"row","fields":[{"name":"a","type":"long"}]}"#;
557
558 fn encoder(schema_id: AvroSchemaId) -> AvroEncoder {
559 let desc = RelationDesc::builder()
560 .with_column("a", SqlScalarType::Int64.nullable(false))
561 .finish();
562 AvroEncoder::new(desc, false, SCHEMA, schema_id)
563 }
564
565 fn row() -> Row {
566 Row::pack_slice(&[Datum::Int64(42)])
567 }
568
569 #[mz_ore::test]
570 fn confluent_framing() {
571 let bytes = encoder(AvroSchemaId::Confluent(7)).encode_unchecked(row());
572 let (id, payload) = crate::confluent::extract_avro_header(&bytes).unwrap();
573 assert_eq!(bytes[0], 0x00, "confluent magic byte");
574 assert_eq!(id, 7);
575 assert!(!payload.is_empty());
576 }
577
578 #[mz_ore::test]
579 fn glue_framing() {
580 let uuid = Uuid::from_u128(0x1234_5678);
581 let bytes = encoder(AvroSchemaId::Glue(uuid)).encode_unchecked(row());
582 let (parsed, payload) = crate::glue::extract_avro_header(&bytes).unwrap();
583 assert_eq!(bytes[0], 0x03, "glue header version");
584 assert_eq!(parsed, uuid);
585 assert!(!payload.is_empty());
586 }
587
588 #[mz_ore::test]
589 fn hash_ignores_framing() {
590 let confluent = encoder(AvroSchemaId::Confluent(7));
594 let glue = encoder(AvroSchemaId::Glue(Uuid::from_u128(0x1234_5678)));
595 let cb = confluent.encode_unchecked(row());
596 let gb = glue.encode_unchecked(row());
597 assert_eq!(confluent.hash(&cb), glue.hash(&gb));
598 }
599}