1use std::sync::Arc;
9
10use arrow_array::builder::{
11 BinaryBuilder, FixedSizeBinaryBuilder, Int32Builder, ListBuilder, StringBuilder, StructBuilder,
12 TimestampMillisecondBuilder,
13};
14use arrow_array::ArrayRef;
15use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef, TimeUnit};
16
17use crate::error::ConnectorError;
18
19pub(super) type RenderedProperties = Vec<(String, Option<Vec<u8>>)>;
21
22pub(super) struct Received {
24 pub(super) data: Vec<u8>,
25 pub(super) md: libmqm_sys::MQMD,
26 pub(super) properties: RenderedProperties,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum MetadataColumn {
32 MsgId,
34 CorrelId,
36 GroupId,
38 MsgSeqNumber,
40 MsgFlags,
42 MsgType,
44 Format,
46 Ccsid,
48 Encoding,
50 Persistence,
52 Priority,
54 Expiry,
56 BackoutCount,
58 ReplyToQueue,
60 ReplyToQueueManager,
62 PutTimestamp,
64 PutApplicationName,
66 UserIdentifier,
68 Properties,
70}
71
72impl MetadataColumn {
73 const ALL: [Self; 19] = [
74 Self::MsgId,
75 Self::CorrelId,
76 Self::GroupId,
77 Self::MsgSeqNumber,
78 Self::MsgFlags,
79 Self::MsgType,
80 Self::Format,
81 Self::Ccsid,
82 Self::Encoding,
83 Self::Persistence,
84 Self::Priority,
85 Self::Expiry,
86 Self::BackoutCount,
87 Self::ReplyToQueue,
88 Self::ReplyToQueueManager,
89 Self::PutTimestamp,
90 Self::PutApplicationName,
91 Self::UserIdentifier,
92 Self::Properties,
93 ];
94
95 #[must_use]
97 pub const fn option_name(self) -> &'static str {
98 match self {
99 Self::MsgId => "msg_id",
100 Self::CorrelId => "correl_id",
101 Self::GroupId => "group_id",
102 Self::MsgSeqNumber => "msg_seq_number",
103 Self::MsgFlags => "msg_flags",
104 Self::MsgType => "msg_type",
105 Self::Format => "format",
106 Self::Ccsid => "ccsid",
107 Self::Encoding => "encoding",
108 Self::Persistence => "persistence",
109 Self::Priority => "priority",
110 Self::Expiry => "expiry",
111 Self::BackoutCount => "backout_count",
112 Self::ReplyToQueue => "reply_to_queue",
113 Self::ReplyToQueueManager => "reply_to_queue_manager",
114 Self::PutTimestamp => "put_timestamp",
115 Self::PutApplicationName => "put_application_name",
116 Self::UserIdentifier => "user_identifier",
117 Self::Properties => "properties",
118 }
119 }
120
121 fn field(self) -> Field {
122 let name = format!("_{}", self.option_name());
123 match self {
124 Self::MsgId | Self::CorrelId | Self::GroupId => {
125 Field::new(name, DataType::FixedSizeBinary(24), false)
126 }
127 Self::Format
128 | Self::ReplyToQueue
129 | Self::ReplyToQueueManager
130 | Self::PutApplicationName
131 | Self::UserIdentifier => Field::new(name, DataType::Utf8, false),
132 Self::PutTimestamp => {
133 Field::new(name, DataType::Timestamp(TimeUnit::Millisecond, None), true)
134 }
135 Self::Properties => Field::new(
136 name,
137 DataType::List(Arc::new(Field::new_struct(
138 "item",
139 property_fields(),
140 false,
141 ))),
142 false,
143 ),
144 Self::MsgSeqNumber
145 | Self::MsgFlags
146 | Self::MsgType
147 | Self::Ccsid
148 | Self::Encoding
149 | Self::Persistence
150 | Self::Priority
151 | Self::Expiry
152 | Self::BackoutCount => Field::new(name, DataType::Int32, false),
153 }
154 }
155}
156
157fn property_fields() -> Fields {
158 Fields::from(vec![
159 Field::new("name", DataType::Utf8, false),
160 Field::new("value", DataType::Binary, true),
161 ])
162}
163
164#[derive(Debug, Clone, Default, PartialEq, Eq)]
166pub struct MetadataColumns(Vec<MetadataColumn>);
167
168impl MetadataColumns {
169 pub fn parse(list: Option<&str>) -> Result<Self, ConnectorError> {
174 let mut columns = Vec::new();
175 for name in list
176 .unwrap_or_default()
177 .split(',')
178 .map(str::trim)
179 .filter(|name| !name.is_empty())
180 {
181 let column = MetadataColumn::ALL
182 .into_iter()
183 .find(|column| column.option_name().eq_ignore_ascii_case(name))
184 .ok_or_else(|| {
185 let known: Vec<&str> = MetadataColumn::ALL
186 .iter()
187 .map(|c| c.option_name())
188 .collect();
189 ConnectorError::ConfigurationError(format!(
190 "unknown IBM MQ metadata column '{name}' (expected one of {})",
191 known.join(", ")
192 ))
193 })?;
194 columns.push(column);
195 }
196 columns.sort_unstable();
197 columns.dedup();
198 Ok(Self(columns))
199 }
200
201 #[must_use]
203 pub fn is_empty(&self) -> bool {
204 self.0.is_empty()
205 }
206
207 #[must_use]
209 pub fn wants_properties(&self) -> bool {
210 self.0.contains(&MetadataColumn::Properties)
211 }
212
213 pub fn validate_payload(&self, payload: &Schema) -> Result<(), ConnectorError> {
218 let collision = payload.fields().iter().find(|field| {
219 self.0.iter().any(|column| {
220 field
221 .name()
222 .eq_ignore_ascii_case(&format!("_{}", column.option_name()))
223 })
224 });
225 match collision {
226 Some(field) => Err(ConnectorError::SchemaMismatch(format!(
227 "IBM MQ payload schema contains configured metadata column '{}'",
228 field.name()
229 ))),
230 None => Ok(()),
231 }
232 }
233
234 #[must_use]
236 pub fn output_schema(&self, payload: &SchemaRef) -> SchemaRef {
237 if self.0.is_empty() {
238 return Arc::clone(payload);
239 }
240 let mut fields = payload.fields().to_vec();
241 fields.extend(self.0.iter().map(|column| Arc::new(column.field())));
242 Arc::new(Schema::new_with_metadata(
243 fields,
244 payload.metadata().clone(),
245 ))
246 }
247
248 pub fn payload_schema(&self, output: &SchemaRef) -> Result<SchemaRef, ConnectorError> {
253 let payload_len = output
254 .fields()
255 .len()
256 .checked_sub(self.0.len())
257 .ok_or_else(|| {
258 ConnectorError::SchemaMismatch("source schema lacks its metadata columns".into())
259 })?;
260 let payload = Arc::new(
261 output
262 .project(&(0..payload_len).collect::<Vec<_>>())
263 .map_err(|error| ConnectorError::SchemaMismatch(error.to_string()))?,
264 );
265 if self.output_schema(&payload) != *output {
266 return Err(ConnectorError::SchemaMismatch(
267 "IBM MQ source schema does not end with its configured metadata columns".into(),
268 ));
269 }
270 Ok(payload)
271 }
272
273 pub(super) fn arrays(&self, messages: &[&Received]) -> Result<Vec<ArrayRef>, ConnectorError> {
275 self.0
276 .iter()
277 .map(|column| column_array(*column, messages))
278 .collect()
279 }
280}
281
282impl std::fmt::Display for MetadataColumns {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 let names: Vec<&str> = self.0.iter().map(|column| column.option_name()).collect();
285 f.write_str(&names.join(","))
286 }
287}
288
289pub(super) fn text(field: &[mqi::types::MQCHAR]) -> String {
291 let bytes: Vec<u8> = field
292 .iter()
293 .map(|character| u8::from_ne_bytes(character.to_ne_bytes()))
294 .collect();
295 String::from_utf8_lossy(&bytes)
296 .trim_end_matches([' ', '\0'])
297 .to_string()
298}
299
300fn column_array(
301 column: MetadataColumn,
302 messages: &[&Received],
303) -> Result<ArrayRef, ConnectorError> {
304 let count = messages.len();
305 let id = |value: fn(&libmqm_sys::MQMD) -> &[u8; 24]| -> Result<ArrayRef, ConnectorError> {
306 let mut builder = FixedSizeBinaryBuilder::with_capacity(count, 24);
307 for message in messages {
308 builder
309 .append_value(value(&message.md))
310 .map_err(|error| ConnectorError::Internal(error.to_string()))?;
311 }
312 Ok(Arc::new(builder.finish()))
313 };
314 let int = |value: fn(&libmqm_sys::MQMD) -> i32| -> ArrayRef {
315 let mut builder = Int32Builder::with_capacity(count);
316 for message in messages {
317 builder.append_value(value(&message.md));
318 }
319 Arc::new(builder.finish())
320 };
321 let string = |value: fn(&libmqm_sys::MQMD) -> String| -> ArrayRef {
322 let mut builder = StringBuilder::with_capacity(count, count * 16);
323 for message in messages {
324 builder.append_value(value(&message.md));
325 }
326 Arc::new(builder.finish())
327 };
328 Ok(match column {
329 MetadataColumn::MsgId => id(|md| &md.MsgId)?,
330 MetadataColumn::CorrelId => id(|md| &md.CorrelId)?,
331 MetadataColumn::GroupId => id(|md| &md.GroupId)?,
332 MetadataColumn::MsgSeqNumber => int(|md| md.MsgSeqNumber),
333 MetadataColumn::MsgFlags => int(|md| md.MsgFlags),
334 MetadataColumn::MsgType => int(|md| md.MsgType),
335 MetadataColumn::Format => string(|md| text(&md.Format)),
336 MetadataColumn::Ccsid => int(|md| md.CodedCharSetId),
337 MetadataColumn::Encoding => int(|md| md.Encoding),
338 MetadataColumn::Persistence => int(|md| md.Persistence),
339 MetadataColumn::Priority => int(|md| md.Priority),
340 MetadataColumn::Expiry => int(|md| md.Expiry),
341 MetadataColumn::BackoutCount => int(|md| md.BackoutCount),
342 MetadataColumn::ReplyToQueue => string(|md| text(&md.ReplyToQ)),
343 MetadataColumn::ReplyToQueueManager => string(|md| text(&md.ReplyToQMgr)),
344 MetadataColumn::PutApplicationName => string(|md| text(&md.PutApplName)),
345 MetadataColumn::UserIdentifier => string(|md| text(&md.UserIdentifier)),
346 MetadataColumn::PutTimestamp => {
347 let mut builder = TimestampMillisecondBuilder::with_capacity(count);
348 for message in messages {
349 builder.append_option(put_timestamp(
350 &text(&message.md.PutDate),
351 &text(&message.md.PutTime),
352 ));
353 }
354 Arc::new(builder.finish())
355 }
356 MetadataColumn::Properties => properties_array(messages),
357 })
358}
359
360pub(super) fn put_timestamp(date: &str, time: &str) -> Option<i64> {
362 let date = chrono::NaiveDate::parse_from_str(date, "%Y%m%d").ok()?;
363 if time.len() != 8 || !time.bytes().all(|byte| byte.is_ascii_digit()) {
364 return None;
365 }
366 let field = |range: std::ops::Range<usize>| time[range].parse::<u32>().ok();
367 let moment =
368 date.and_hms_milli_opt(field(0..2)?, field(2..4)?, field(4..6)?, field(6..8)? * 10)?;
369 Some(moment.and_utc().timestamp_millis())
370}
371
372fn properties_array(messages: &[&Received]) -> ArrayRef {
373 let entry = StructBuilder::new(
374 property_fields(),
375 vec![
376 Box::new(StringBuilder::new()),
377 Box::new(BinaryBuilder::new()),
378 ],
379 );
380 let mut builder = ListBuilder::new(entry).with_field(Arc::new(Field::new_struct(
381 "item",
382 property_fields(),
383 false,
384 )));
385 for message in messages {
386 for (name, value) in &message.properties {
387 let entry = builder.values();
388 entry
389 .field_builder::<StringBuilder>(0)
390 .expect("property name builder is Utf8")
391 .append_value(name);
392 entry
393 .field_builder::<BinaryBuilder>(1)
394 .expect("property value builder is Binary")
395 .append_option(value.as_deref());
396 entry.append(true);
397 }
398 builder.append(true);
399 }
400 Arc::new(builder.finish())
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn put_timestamps_use_hundredths_and_reject_blanks() {
409 assert_eq!(
410 put_timestamp("20261017", "10305912"),
411 Some(
412 chrono::NaiveDate::from_ymd_opt(2026, 10, 17)
413 .unwrap()
414 .and_hms_milli_opt(10, 30, 59, 120)
415 .unwrap()
416 .and_utc()
417 .timestamp_millis()
418 )
419 );
420 assert_eq!(put_timestamp("", ""), None);
421 assert_eq!(put_timestamp("20261017", "2530xx00"), None);
422 }
423
424 #[test]
425 fn character_fields_trim_mq_padding() {
426 let raw: Vec<mqi::types::MQCHAR> = b"MQSTR "
427 .iter()
428 .map(|byte| mqi::types::MQCHAR::from_ne_bytes(byte.to_ne_bytes()))
429 .collect();
430 assert_eq!(text(&raw), "MQSTR");
431 }
432
433 #[test]
434 fn metadata_columns_are_canonical_and_round_trip() {
435 let columns = MetadataColumns::parse(Some("backout_count,msg_id,MSG_ID")).unwrap();
436 assert_eq!(columns.to_string(), "msg_id,backout_count");
437 let payload = Arc::new(Schema::new(vec![Field::new("v", DataType::Binary, true)]));
438 let output = columns.output_schema(&payload);
439 assert_eq!(output.field(1).data_type(), &DataType::FixedSizeBinary(24));
440 assert_eq!(columns.payload_schema(&output).unwrap(), payload);
441 assert!(MetadataColumns::parse(Some("offset")).is_err());
442 }
443}