1use std::cmp;
11use std::io::{BufRead, Read};
12use std::time::Duration;
13
14use anyhow::{Context, anyhow, bail};
15use byteorder::{NetworkEndian, WriteBytesExt};
16use futures::stream::{FuturesUnordered, StreamExt};
17use maplit::btreemap;
18use prost::Message;
19use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor};
20use rdkafka::message::{Header, OwnedHeaders};
21use rdkafka::producer::FutureRecord;
22use serde::de::DeserializeOwned;
23use tokio::fs;
24use uuid::Uuid;
25
26use crate::action::{self, ControlFlow, State};
27use crate::format::avro::{self, Schema};
28use crate::format::bytes;
29use crate::parser::BuiltinCommand;
30
31const INGEST_BATCH_SIZE: isize = 10000;
32
33#[allow(clippy::disallowed_types)]
36fn extract_all_defined_types(
37 schema_json: &str,
38) -> anyhow::Result<std::collections::HashSet<String>> {
39 let value: serde_json::Value = serde_json::from_str(schema_json)
40 .context("parsing schema JSON to extract defined types")?;
41
42 let mut types = std::collections::HashSet::new();
43 collect_defined_types(&value, None, &mut types);
44 Ok(types)
45}
46
47#[allow(clippy::disallowed_types)]
49fn collect_defined_types(
50 value: &serde_json::Value,
51 parent_namespace: Option<&str>,
52 types: &mut std::collections::HashSet<String>,
53) {
54 match value {
55 serde_json::Value::Object(map) => {
56 let namespace = map
58 .get("namespace")
59 .and_then(|v| v.as_str())
60 .or(parent_namespace);
61
62 if let Some(type_val) = map.get("type")
64 && type_val
65 .as_str()
66 .is_some_and(|typ| ["record", "enum", "fixed"].contains(&typ))
67 {
68 if let Some(name) = map.get("name").and_then(|v| v.as_str()) {
69 let fullname = if name.contains('.') {
71 name.to_string()
72 } else if let Some(ns) = namespace {
73 format!("{}.{}", ns, name)
74 } else {
75 name.to_string()
76 };
77 types.insert(fullname);
78 }
79 }
80
81 for entity_type in &["type", "items", "values", "fields"] {
84 if let Some(val) = map.get(*entity_type) {
85 collect_defined_types(val, namespace, types);
86 }
87 }
88 }
89 serde_json::Value::Array(arr) => {
90 for item in arr {
91 collect_defined_types(item, parent_namespace, types);
92 }
93 }
94 _ => {}
95 }
96}
97
98#[allow(clippy::disallowed_types)]
101fn extract_type_references(schema_json: &str) -> anyhow::Result<std::collections::HashSet<String>> {
102 let value: serde_json::Value = serde_json::from_str(schema_json)
103 .context("parsing schema JSON to extract type references")?;
104
105 let mut references = std::collections::HashSet::new();
106 collect_type_references(&value, &mut references);
107 Ok(references)
108}
109
110#[allow(clippy::disallowed_types)]
112fn collect_type_references(
113 value: &serde_json::Value,
114 references: &mut std::collections::HashSet<String>,
115) {
116 match value {
117 serde_json::Value::String(s) => {
118 if s.contains('.')
120 && ![
121 "null", "boolean", "int", "long", "float", "double", "bytes", "string",
122 ]
123 .contains(&s.as_str())
124 {
125 references.insert(s.clone());
126 }
127 }
128 serde_json::Value::Object(map) => {
129 if let Some(type_val) = map.get("type")
132 && type_val
133 .as_str()
134 .is_some_and(|typ| ["record", "enum", "fixed"].contains(&typ))
135 {
136 if let Some(fields) = map.get("fields") {
137 collect_type_references(fields, references);
138 }
139 return;
140 }
141
142 for entity_type in &["type", "items", "values", "fields"] {
145 if let Some(val) = map.get(*entity_type) {
146 collect_type_references(val, references);
147 }
148 }
149 }
150 serde_json::Value::Array(arr) => {
151 for item in arr {
152 collect_type_references(item, references);
153 }
154 }
155 _ => {}
156 }
157}
158
159#[derive(Clone)]
160enum Format {
161 Avro {
162 schema: String,
163 confluent_wire_format: bool,
164 glue_schema_version_id: Option<Uuid>,
168 references: Vec<String>,
170 },
171 Protobuf {
172 descriptor_file: String,
173 message: String,
174 confluent_wire_format: bool,
175 schema_id_subject: Option<String>,
176 schema_message_id: u8,
177 },
178 Bytes {
179 terminator: Option<u8>,
180 },
181}
182
183enum Transcoder {
184 PlainAvro {
185 schema: Schema,
186 },
187 ConfluentAvro {
188 schema: Schema,
189 schema_id: i32,
190 },
191 GlueAvro {
192 schema: Schema,
193 schema_version_id: Uuid,
194 },
195 Protobuf {
196 message: MessageDescriptor,
197 confluent_wire_format: bool,
198 schema_id: i32,
199 schema_message_id: u8,
200 },
201 Bytes {
202 terminator: Option<u8>,
203 },
204}
205
206impl Transcoder {
207 fn decode_json<R, T>(row: R) -> Result<Option<T>, anyhow::Error>
208 where
209 R: Read,
210 T: DeserializeOwned,
211 {
212 let deserializer = serde_json::Deserializer::from_reader(row);
213 deserializer
214 .into_iter()
215 .next()
216 .transpose()
217 .context("parsing json")
218 }
219
220 fn transcode<R>(&self, mut row: R) -> Result<Option<Vec<u8>>, anyhow::Error>
221 where
222 R: BufRead,
223 {
224 match self {
225 Transcoder::ConfluentAvro { schema, schema_id } => {
226 if let Some(val) = Self::decode_json(row)? {
227 let val = avro::from_json(&val, schema.top_node())?;
228 let mut out = vec![];
229 out.write_u8(0).unwrap();
235 out.write_i32::<NetworkEndian>(*schema_id).unwrap();
236 out.extend(avro::to_avro_datum(schema, val)?);
237 Ok(Some(out))
238 } else {
239 Ok(None)
240 }
241 }
242 Transcoder::GlueAvro {
243 schema,
244 schema_version_id,
245 } => {
246 if let Some(val) = Self::decode_json(row)? {
247 let val = avro::from_json(&val, schema.top_node())?;
248 let mut out = vec![];
249 out.write_u8(0x03).unwrap();
254 out.write_u8(0x00).unwrap();
255 out.extend_from_slice(schema_version_id.as_bytes());
256 out.extend(avro::to_avro_datum(schema, val)?);
257 Ok(Some(out))
258 } else {
259 Ok(None)
260 }
261 }
262 Transcoder::PlainAvro { schema } => {
263 if let Some(val) = Self::decode_json(row)? {
264 let val = avro::from_json(&val, schema.top_node())?;
265 let mut out = vec![];
266 out.extend(avro::to_avro_datum(schema, val)?);
267 Ok(Some(out))
268 } else {
269 Ok(None)
270 }
271 }
272 Transcoder::Protobuf {
273 message,
274 confluent_wire_format,
275 schema_id,
276 schema_message_id,
277 } => {
278 if let Some(val) = Self::decode_json::<_, serde_json::Value>(row)? {
279 let message = DynamicMessage::deserialize(message.clone(), val)
280 .context("parsing protobuf JSON")?;
281 let mut out = vec![];
282 if *confluent_wire_format {
283 out.write_u8(0).unwrap();
290 out.write_i32::<NetworkEndian>(*schema_id).unwrap();
291 out.write_u8(*schema_message_id).unwrap();
292 }
293 message.encode(&mut out)?;
294 Ok(Some(out))
295 } else {
296 Ok(None)
297 }
298 }
299 Transcoder::Bytes { terminator } => {
300 let mut out = vec![];
301 match terminator {
302 Some(t) => {
303 row.read_until(*t, &mut out)?;
304 if out.last() == Some(t) {
305 out.pop();
306 }
307 }
308 None => {
309 row.read_to_end(&mut out)?;
310 }
311 }
312 if out.is_empty() {
313 Ok(None)
314 } else {
315 Ok(Some(bytes::unescape(&out)?))
316 }
317 }
318 }
319 }
320}
321
322pub async fn run_ingest(
323 mut cmd: BuiltinCommand,
324 state: &mut State,
325) -> Result<ControlFlow, anyhow::Error> {
326 let topic_prefix = format!("testdrive-{}", cmd.args.string("topic")?);
327 let partition = cmd.args.opt_parse::<i32>("partition")?;
328 let start_iteration = cmd.args.opt_parse::<isize>("start-iteration")?.unwrap_or(0);
329 let repeat = cmd.args.opt_parse::<isize>("repeat")?.unwrap_or(1);
330 let omit_key = cmd.args.opt_bool("omit-key")?.unwrap_or(false);
331 let omit_value = cmd.args.opt_bool("omit-value")?.unwrap_or(false);
332 let schema_id_var = cmd.args.opt_parse("set-schema-id-var")?;
333 let key_schema_id_var = cmd.args.opt_parse("set-key-schema-id-var")?;
334 let confluent_wire_format_arg = cmd.args.opt_bool("confluent-wire-format")?;
338 let format = match cmd.args.string("format")?.as_str() {
339 "avro" => {
340 let glue_schema_version_id = cmd
341 .args
342 .opt_string("glue-schema-version-id")
343 .map(|s| Uuid::parse_str(&s).context("parsing glue-schema-version-id as UUID"))
344 .transpose()?;
345 let confluent_wire_format_explicit = confluent_wire_format_arg;
346 if glue_schema_version_id.is_some() && confluent_wire_format_explicit == Some(true) {
347 bail!("confluent-wire-format=true is incompatible with glue-schema-version-id");
348 }
349 let confluent_wire_format =
351 confluent_wire_format_explicit.unwrap_or_else(|| glue_schema_version_id.is_none());
352 Format::Avro {
353 schema: cmd.args.string("schema")?,
354 confluent_wire_format,
355 glue_schema_version_id,
356 references: cmd
357 .args
358 .opt_string("references")
359 .map(|s| s.split(',').map(|s| s.to_string()).collect())
360 .unwrap_or_default(),
361 }
362 }
363 "protobuf" => {
364 let descriptor_file = cmd.args.string("descriptor-file")?;
365 let message = cmd.args.string("message")?;
366 Format::Protobuf {
367 descriptor_file,
368 message,
369 confluent_wire_format: confluent_wire_format_arg.unwrap_or(false),
372 schema_id_subject: cmd.args.opt_string("schema-id-subject"),
373 schema_message_id: cmd.args.opt_parse::<u8>("schema-message-id")?.unwrap_or(0),
374 }
375 }
376 "bytes" => Format::Bytes { terminator: None },
377 f => bail!("unknown format: {}", f),
378 };
379 let mut key_schema = cmd.args.opt_string("key-schema");
380 let key_format = match cmd.args.opt_string("key-format").as_deref() {
381 Some("avro") => {
382 let key_glue_schema_version_id = cmd
383 .args
384 .opt_string("key-glue-schema-version-id")
385 .map(|s| Uuid::parse_str(&s).context("parsing key-glue-schema-version-id as UUID"))
386 .transpose()?;
387 let confluent_wire_format_explicit = confluent_wire_format_arg;
388 if key_glue_schema_version_id.is_some() && confluent_wire_format_explicit == Some(true)
389 {
390 bail!("confluent-wire-format=true is incompatible with key-glue-schema-version-id");
391 }
392 let confluent_wire_format = confluent_wire_format_explicit
394 .unwrap_or_else(|| key_glue_schema_version_id.is_none());
395 Some(Format::Avro {
396 schema: key_schema.take().ok_or_else(|| {
397 anyhow!("key-schema parameter required when key-format is present")
398 })?,
399 confluent_wire_format,
400 glue_schema_version_id: key_glue_schema_version_id,
401 references: cmd
402 .args
403 .opt_string("key-references")
404 .map(|s| s.split(',').map(|s| s.to_string()).collect())
405 .unwrap_or_default(),
406 })
407 }
408 Some("protobuf") => {
409 let descriptor_file = cmd.args.string("key-descriptor-file")?;
410 let message = cmd.args.string("key-message")?;
411 Some(Format::Protobuf {
412 descriptor_file,
413 message,
414 confluent_wire_format: confluent_wire_format_arg.unwrap_or(false),
415 schema_id_subject: cmd.args.opt_string("key-schema-id-subject"),
416 schema_message_id: cmd
417 .args
418 .opt_parse::<u8>("key-schema-message-id")?
419 .unwrap_or(0),
420 })
421 }
422 Some("bytes") => Some(Format::Bytes {
423 terminator: match cmd.args.opt_parse::<char>("key-terminator")? {
424 Some(c) => match u8::try_from(c) {
425 Ok(c) => Some(c),
426 Err(_) => bail!("key terminator must be single ASCII character"),
427 },
428 None => Some(b':'),
429 },
430 }),
431 Some(f) => bail!("unknown key format: {}", f),
432 None => None,
433 };
434 if key_schema.is_some() {
435 anyhow::bail!("key-schema specified without a matching key-format");
436 }
437
438 let timestamp = cmd.args.opt_parse("timestamp")?;
439
440 use serde_json::Value;
441 let headers = if let Some(headers_val) = cmd.args.opt_parse::<serde_json::Value>("headers")? {
442 let mut headers = Vec::new();
443 let headers_maps = match headers_val {
444 Value::Array(values) => {
445 let mut headers_map = Vec::new();
446 for value in values {
447 if let Value::Object(m) = value {
448 headers_map.push(m)
449 } else {
450 bail!("`headers` array values must be maps")
451 }
452 }
453 headers_map
454 }
455 Value::Object(v) => vec![v],
456 _ => bail!("`headers` must be a map or an array"),
457 };
458
459 for headers_map in headers_maps {
460 for (k, v) in headers_map.iter() {
461 headers.push((k.clone(), match v {
462 Value::String(val) => Some(val.as_bytes().to_vec()),
463 Value::Array(val) => {
464 let mut values = Vec::new();
465 for value in val {
466 if let Value::Number(int) = value {
467 values.push(u8::try_from(int.as_i64().unwrap()).unwrap())
468 } else {
469 bail!("`headers` value arrays must only contain numbers (to represent bytes)")
470 }
471 }
472 Some(values.clone())
473 },
474 Value::Null => None,
475 _ => bail!("`headers` must have string, int array or null values")
476 }));
477 }
478 }
479 Some(headers)
480 } else {
481 None
482 };
483
484 cmd.args.done()?;
485
486 if let Some(kf) = &key_format {
487 fn is_confluent_format(fmt: &Format) -> Option<bool> {
488 match fmt {
489 Format::Avro {
490 confluent_wire_format,
491 glue_schema_version_id,
492 ..
493 } => {
494 if glue_schema_version_id.is_some() {
496 None
497 } else {
498 Some(*confluent_wire_format)
499 }
500 }
501 Format::Protobuf {
502 confluent_wire_format,
503 ..
504 } => Some(*confluent_wire_format),
505 Format::Bytes { .. } => None,
506 }
507 }
508 match (is_confluent_format(kf), is_confluent_format(&format)) {
509 (Some(false), Some(true)) | (Some(true), Some(false)) => {
510 bail!(
511 "It does not make sense to have the key be in confluent format and not the value, or vice versa."
512 );
513 }
514 _ => {}
515 }
516 }
517
518 let topic_name = &format!("{}-{}", topic_prefix, state.seed);
519 println!(
520 "Ingesting data into Kafka topic {} with start_iteration = {}, repeat = {}",
521 topic_name, start_iteration, repeat
522 );
523
524 let set_schema_id_var = |state: &mut State, schema_id_var, transcoder| match transcoder {
525 &Transcoder::ConfluentAvro { schema_id, .. } | &Transcoder::Protobuf { schema_id, .. } => {
526 state.cmd_vars.insert(schema_id_var, schema_id.to_string());
527 }
528 _ => (),
529 };
530
531 let value_transcoder =
532 make_transcoder(state, format.clone(), format!("{}-value", topic_name)).await?;
533 if let Some(var) = schema_id_var {
534 set_schema_id_var(state, var, &value_transcoder);
535 }
536
537 let key_transcoder = match key_format.clone() {
538 None => None,
539 Some(f) => {
540 let transcoder = make_transcoder(state, f, format!("{}-key", topic_name)).await?;
541 if let Some(var) = key_schema_id_var {
542 set_schema_id_var(state, var, &transcoder);
543 }
544 Some(transcoder)
545 }
546 };
547
548 let mut futs = FuturesUnordered::new();
549
550 for iteration in start_iteration..(start_iteration + repeat) {
551 let iter = &mut cmd.input.iter().peekable();
552
553 for row in iter {
554 let row = action::substitute_vars(
555 row,
556 &btreemap! { "kafka-ingest.iteration".into() => iteration.to_string() },
557 &None,
558 false,
559 )?;
560 let mut row = row.as_bytes();
561 let key = match (omit_key, &key_transcoder) {
562 (true, _) => None,
563 (false, None) => None,
564 (false, Some(kt)) => kt.transcode(&mut row)?,
565 };
566 let value = if omit_value {
567 None
568 } else {
569 value_transcoder
570 .transcode(&mut row)
571 .with_context(|| format!("parsing row: {}", String::from_utf8_lossy(row)))?
572 };
573 let producer = &state.kafka_producer;
574 let timeout = cmp::max(state.default_timeout, Duration::from_secs(1));
575 let headers = headers.clone();
576 futs.push(async move {
577 let mut record: FutureRecord<_, _> = FutureRecord::to(topic_name);
578
579 if let Some(partition) = partition {
580 record = record.partition(partition);
581 }
582 if let Some(key) = &key {
583 record = record.key(key);
584 }
585 if let Some(value) = &value {
586 record = record.payload(value);
587 }
588 if let Some(timestamp) = timestamp {
589 record = record.timestamp(timestamp);
590 }
591 if let Some(headers) = headers {
592 let mut rd_meta = OwnedHeaders::new();
593 for (k, v) in &headers {
594 rd_meta = rd_meta.insert(Header {
595 key: k,
596 value: v.as_deref(),
597 });
598 }
599 record = record.headers(rd_meta);
600 }
601 producer.send(record, timeout).await
602 });
603 }
604
605 if iteration % INGEST_BATCH_SIZE == 0 || iteration == (start_iteration + repeat - 1) {
607 while let Some(res) = futs.next().await {
608 res.map_err(|(e, _message)| e)?;
609 }
610 }
611 }
612 Ok(ControlFlow::Continue)
613}
614
615async fn make_transcoder(
616 state: &State,
617 format: Format,
618 ccsr_subject: String,
619) -> Result<Transcoder, anyhow::Error> {
620 match format {
621 Format::Avro {
622 schema,
623 confluent_wire_format,
624 glue_schema_version_id,
625 references,
626 } => {
627 if let Some(schema_version_id) = glue_schema_version_id {
628 if !references.is_empty() {
629 bail!("schema references are not supported with glue-schema-version-id");
630 }
631 let schema = avro::parse_schema(&schema, &[])
632 .with_context(|| format!("parsing avro schema: {}", schema))?;
633 return Ok(Transcoder::GlueAvro {
634 schema,
635 schema_version_id,
636 });
637 }
638 if confluent_wire_format {
639 #[allow(clippy::disallowed_types)]
643 let mut reference_subjects = vec![];
644 #[allow(clippy::disallowed_types)]
645 let mut seen_subjects: std::collections::HashSet<String> =
646 std::collections::HashSet::new();
647 let mut queue: Vec<String> = references.clone();
648
649 while let Some(ref_name) = queue.pop() {
651 if seen_subjects.contains(&ref_name) {
652 continue;
653 }
654 seen_subjects.insert(ref_name.clone());
655
656 let (subject, ref_deps) = state
657 .ccsr_client
658 .get_subject_with_references(&ref_name)
659 .await
660 .with_context(|| format!("fetching reference {}", ref_name))?;
661
662 for dep in ref_deps {
664 if !seen_subjects.contains(&dep.subject) {
665 queue.push(dep.subject);
666 }
667 }
668
669 let defined_types = extract_all_defined_types(&subject.schema.raw)
671 .with_context(|| {
672 format!("extracting type names from reference schema {}", ref_name)
673 })?;
674 reference_subjects.push((
675 ref_name,
676 subject.version,
677 subject.schema.raw,
678 defined_types,
679 ));
680 }
681
682 reference_subjects.reverse();
686
687 let direct_refs = extract_type_references(&schema)
689 .context("extracting type references from schema")?;
690
691 let mut schema_references = vec![];
694 for type_name in &direct_refs {
695 for (subject_name, version, _, defined_types) in &reference_subjects {
696 if defined_types.contains(type_name) {
697 schema_references.push(mz_ccsr::SchemaReference {
698 name: type_name.clone(),
699 subject: subject_name.clone(),
700 version: *version,
701 });
702 break;
703 }
704 }
705 }
706
707 let reference_raw_schemas: Vec<_> = reference_subjects
709 .into_iter()
710 .map(|(_, _, raw, _)| raw)
711 .collect();
712
713 let schema_id = state
714 .ccsr_client
715 .publish_schema(
716 &ccsr_subject,
717 &schema,
718 mz_ccsr::SchemaType::Avro,
719 &schema_references,
720 )
721 .await
722 .context("publishing to schema registry")?;
723
724 let schema = if reference_raw_schemas.is_empty() {
726 avro::parse_schema(&schema, &[])
727 .with_context(|| format!("parsing avro schema: {}", schema))?
728 } else {
729 let mut parsed_refs: Vec<Schema> = vec![];
732 for raw in &reference_raw_schemas {
733 let schema_value: serde_json::Value = serde_json::from_str(raw)
734 .with_context(|| format!("parsing reference schema JSON: {}", raw))?;
735 let parsed = Schema::parse_with_references(&schema_value, &parsed_refs)
736 .with_context(|| format!("parsing reference avro schema: {}", raw))?;
737 parsed_refs.push(parsed);
738 }
739
740 let schema_value: serde_json::Value = serde_json::from_str(&schema)
742 .with_context(|| format!("parsing schema JSON: {}", schema))?;
743 Schema::parse_with_references(&schema_value, &parsed_refs).with_context(
744 || format!("parsing avro schema with references: {}", schema),
745 )?
746 };
747
748 Ok::<_, anyhow::Error>(Transcoder::ConfluentAvro { schema, schema_id })
749 } else {
750 let schema = avro::parse_schema(&schema, &[])
751 .with_context(|| format!("parsing avro schema: {}", schema))?;
752 Ok(Transcoder::PlainAvro { schema })
753 }
754 }
755 Format::Protobuf {
756 descriptor_file,
757 message,
758 confluent_wire_format,
759 schema_id_subject,
760 schema_message_id,
761 } => {
762 let schema_id = if confluent_wire_format {
763 state
764 .ccsr_client
765 .get_schema_by_subject(schema_id_subject.as_deref().unwrap_or(&ccsr_subject))
766 .await
767 .context("fetching schema from registry")?
768 .id
769 } else {
770 0
771 };
772
773 let bytes = fs::read(state.temp_path.join(descriptor_file))
774 .await
775 .context("reading protobuf descriptor file")?;
776 let fd = DescriptorPool::decode(&*bytes).context("parsing protobuf descriptor file")?;
777 let message = fd
778 .get_message_by_name(&message)
779 .ok_or_else(|| anyhow!("unknown message name {}", message))?;
780 Ok(Transcoder::Protobuf {
781 message,
782 confluent_wire_format,
783 schema_id,
784 schema_message_id,
785 })
786 }
787 Format::Bytes { terminator } => Ok(Transcoder::Bytes { terminator }),
788 }
789}