1use std::fmt;
5use std::marker::PhantomData;
6use std::mem;
7
8#[cfg(feature = "chrono")]
9use chrono::{DateTime, Utc};
10use ruststream::HeaderMap;
11use sqlx::{Column, Database, Error, FromRow, Row, ValueRef};
12#[cfg(feature = "time")]
13use time::OffsetDateTime;
14
15use super::by_name::ByName;
16use super::database::RoleColumns;
17use super::kinds::{IntKind, Kinds};
18use crate::inbox::engine::{Claimed, undecodable};
19use crate::inbox::{PayloadLane, PayloadRow, QueueRow};
20
21#[doc(hidden)]
25pub struct NamedRow<D> {
26 id: NamedId,
27 payload: NamedBytes,
28 headers: HeaderMap,
30 key: Option<NamedBytes>,
31 attempt: Option<u64>,
32 fits: Fits,
33 dialect: PhantomData<fn() -> D>,
34}
35
36impl<D> fmt::Debug for NamedRow<D> {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.debug_struct("NamedRow")
39 .field("id", &self.id)
40 .field("payload", &self.payload)
41 .field("headers", &self.headers)
42 .field("key", &self.key)
43 .field("attempt", &self.attempt)
44 .field("fits", &self.fits)
45 .finish()
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52struct Fits {
53 id: u8,
54 payload: u8,
55 key: u8,
57 attempt: u8,
58}
59
60impl<D> NamedRow<D> {
61 fn unfit(&self, kinds: Kinds) -> Option<(&'static str, &'static str)> {
64 if self.fits.id & kinds.id.bit() == 0 {
65 return Some(("id", kinds.id.name()));
66 }
67 if self.fits.payload & kinds.payload.bit() == 0 {
68 return Some(("payload", kinds.payload.name()));
69 }
70 if let Some(key) = kinds.key
71 && self.fits.key & key.bit() == 0
72 {
73 return Some(("partition_key", key.name()));
74 }
75 if let Some(attempt) = kinds.attempt
76 && !attempt_fits(attempt, self.fits.attempt)
77 {
78 return Some(("attempt", attempt.name()));
79 }
80 None
81 }
82
83 fn fitting_attempt(&self, kinds: Kinds) -> Option<u64> {
86 let kind = kinds.attempt?;
87 self.attempt
88 .filter(|_| attempt_fits(kind, self.fits.attempt))
89 }
90}
91
92const fn attempt_fits(kind: IntKind, held: u8) -> bool {
94 held & kind.bit() != 0
95}
96
97fn hold_to<D: 'static>(kinds: Kinds, claimed: &mut Claimed<NamedRow<D>>) -> Result<(), Error> {
101 let Claimed::Row(row) = claimed else {
102 return Ok(());
103 };
104 let Some((alias, read_as)) = row.unfit(kinds) else {
105 return Ok(());
106 };
107 let error = Error::ColumnDecode {
108 index: format!("{alias:?}"),
109 source: format!(
110 "the struct reads this column as {read_as}, which its type in the table does not hold"
111 )
112 .into(),
113 };
114 if alias == "id" {
115 return Err(error);
116 }
117 let attempt = row.fitting_attempt(kinds);
119 let id = mem::replace(&mut row.id, NamedId::I64(0));
120 *claimed = Claimed::Undecodable {
121 id,
122 attempt,
123 error: Box::new(undecodable(error)),
124 };
125 Ok(())
126}
127
128#[doc(hidden)]
130#[derive(PartialEq, Eq, Hash)]
131pub enum NamedId {
132 I16(i16),
134 I32(i32),
136 I64(i64),
138 Text(String),
140 Bytes(Vec<u8>),
142}
143
144impl Clone for NamedId {
147 fn clone(&self) -> Self {
148 match self {
149 Self::I16(id) => Self::I16(*id),
150 Self::I32(id) => Self::I32(*id),
151 Self::I64(id) => Self::I64(*id),
152 Self::Text(id) => Self::Text(id.clone()),
153 Self::Bytes(id) => Self::Bytes(id.clone()),
154 }
155 }
156
157 fn clone_from(&mut self, source: &Self) {
158 match (self, source) {
159 (Self::Text(kept), Self::Text(id)) => kept.clone_from(id),
160 (Self::Bytes(kept), Self::Bytes(id)) => kept.clone_from(id),
161 (kept, id) => *kept = id.clone(),
162 }
163 }
164}
165
166impl fmt::Debug for NamedId {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 Self::I16(id) => fmt::Debug::fmt(id, f),
172 Self::I32(id) => fmt::Debug::fmt(id, f),
173 Self::I64(id) => fmt::Debug::fmt(id, f),
174 Self::Text(id) => fmt::Debug::fmt(id, f),
175 Self::Bytes(id) => fmt::Debug::fmt(id, f),
176 }
177 }
178}
179
180#[doc(hidden)]
182#[derive(Debug, Clone, PartialEq, Eq, Hash)]
183pub enum NamedBytes {
184 Bytes(Vec<u8>),
186 Text(String),
188}
189
190impl NamedBytes {
191 fn as_bytes(&self) -> &[u8] {
192 match self {
193 Self::Bytes(bytes) => bytes,
194 Self::Text(text) => text.as_bytes(),
195 }
196 }
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292#[non_exhaustive]
293pub enum NamedTime {
294 #[cfg(feature = "chrono")]
296 Chrono(DateTime<Utc>),
297 #[cfg(feature = "time")]
299 Time(OffsetDateTime),
300}
301
302fn read<DB, D>(row: &DB::Row) -> Result<NamedRow<D>, Error>
305where
306 DB: RoleColumns,
307 D: ByName<DB>,
308{
309 let mut id = None;
310 let mut payload = None;
311 let mut headers = HeaderMap::new();
312 let mut key = None;
313 let mut attempt = None;
314 let mut fits = Fits {
315 id: 0,
316 payload: 0,
317 key: 0,
318 attempt: 0,
319 };
320 for column in row.columns() {
321 let alias = column.name();
322 let failed = |source| Error::ColumnDecode {
323 index: format!("{alias:?}"),
324 source,
325 };
326 let value = || DB::value(row, column.ordinal());
327 match alias {
328 "id" => {
329 let (read, held) = DB::id(value()?).map_err(failed)?;
330 (id, fits.id) = (Some(read), held);
331 }
332 "payload" => {
333 let (read, held) = DB::bytes(value()?).map_err(failed)?;
334 (payload, fits.payload) = (Some(read), held);
335 }
336 "partition_key" => {
337 let value = value()?;
338 if value.is_null() {
339 fits.key = u8::MAX;
340 } else {
341 let (read, held) = DB::bytes(value).map_err(failed)?;
342 (key, fits.key) = (Some(read), held);
343 }
344 }
345 "headers" => {
346 let value = value()?;
347 if !value.is_null() {
348 headers = D::headers(value).map_err(failed)?;
349 }
350 }
351 "attempt" => {
352 let (read, held) = DB::attempt(value()?).map_err(failed)?;
353 (attempt, fits.attempt) = (Some(read), held);
354 }
355 _ => {}
356 }
357 }
358 Ok(NamedRow {
359 id: id.ok_or_else(|| Error::ColumnNotFound("id".to_owned()))?,
360 payload: payload.ok_or_else(|| Error::ColumnNotFound("payload".to_owned()))?,
361 headers,
362 key,
363 attempt,
364 fits,
365 dialect: PhantomData,
366 })
367}
368
369impl<'r, R, D> FromRow<'r, R> for NamedRow<D>
370where
371 R: Row,
372 R::Database: RoleColumns + Database<Row = R>,
373 D: ByName<R::Database>,
374{
375 fn from_row(row: &'r R) -> Result<Self, Error> {
376 read::<R::Database, D>(row)
377 }
378}
379
380impl<D: 'static> QueueRow for NamedRow<D> {
381 type Id = NamedId;
382 type Lane = PayloadLane;
383}
384
385impl<D: 'static> PayloadRow for NamedRow<D> {
386 type Column = NamedBytes;
387
388 fn payload(&self) -> &[u8] {
389 self.payload.as_bytes()
390 }
391}
392
393mod events;
394
395#[cfg(test)]
396mod tests {
397 use std::marker::PhantomData;
400
401 use ruststream::HeaderMap;
402 use ruststream::codec::CodecError;
403 use sqlx::Error;
404
405 use super::{Fits, NamedBytes, NamedId, NamedRow, hold_to};
406 use crate::inbox::engine::{Claimed, undecodable};
407 use crate::inbox::named::kinds::{BytesKind, ClockKind, IdKind, IntKind, Kinds};
408
409 pub(super) const KINDS: Kinds = Kinds {
411 clock: ClockKind::System,
412 id: IdKind::I64,
413 payload: BytesKind::Bytes,
414 key: Some(BytesKind::Text),
415 attempt: Some(IntKind::I16),
416 retry_after: None,
417 processed_at: None,
418 locked_until: None,
419 };
420
421 pub(super) fn row<D>(fits: Fits) -> NamedRow<D> {
423 NamedRow {
424 id: NamedId::I64(7),
425 payload: NamedBytes::Bytes(b"{}".to_vec()),
426 headers: HeaderMap::new(),
427 key: Some(NamedBytes::Text("acme".to_owned())),
428 attempt: Some(2),
429 fits,
430 dialect: PhantomData,
431 }
432 }
433
434 pub(super) const FITTING: Fits = Fits {
435 id: IdKind::I64.bit(),
436 payload: BytesKind::Bytes.bit(),
437 key: BytesKind::Text.bit(),
438 attempt: IntKind::I16.bit(),
439 };
440
441 fn driver_error(error: &CodecError) -> Option<&Error> {
443 match error {
444 CodecError::Decode(source) => source.downcast_ref::<Error>(),
445 _ => None,
446 }
447 }
448
449 fn unfit_column(claimed: &Claimed<NamedRow<()>>) -> Option<(String, Option<u64>)> {
451 match claimed {
452 Claimed::Undecodable { id, attempt, error } => {
453 assert_eq!(id, &NamedId::I64(7), "the row keeps its id for the policy");
454 match driver_error(error) {
455 Some(Error::ColumnDecode { index, .. }) => Some((index.clone(), *attempt)),
456 other => panic!("not a column's error: {other:?}"),
457 }
458 }
459 Claimed::Row(_) | Claimed::Missing(_) => None,
460 }
461 }
462
463 #[test]
464 fn logs_name_a_row_by_its_id_as_the_table_holds_it() {
465 let ids = [
466 NamedId::I16(1),
467 NamedId::I32(2),
468 NamedId::I64(3),
469 NamedId::Text("job-4".to_owned()),
470 NamedId::Bytes(vec![5]),
471 ];
472 let logged: Vec<String> = ids.iter().map(|id| format!("{id:?}")).collect();
473 assert_eq!(logged, ["1", "2", "3", "\"job-4\"", "[5]"]);
474 }
475
476 #[test]
477 fn an_id_is_copied_into_the_storage_of_the_one_it_replaces() {
478 let mut kept = NamedId::Text("job-0001".to_owned());
479 let storage = match &kept {
480 NamedId::Text(text) => text.as_ptr(),
481 other => panic!("not a text id: {other:?}"),
482 };
483 kept.clone_from(&NamedId::Text("job-0002".to_owned()));
484 assert!(
485 matches!(&kept, NamedId::Text(text) if text == "job-0002" && text.as_ptr() == storage),
486 "{kept:?}"
487 );
488 let mut bytes = NamedId::Bytes(vec![1, 2]);
489 bytes.clone_from(&NamedId::Bytes(vec![3, 4]));
490 assert_eq!(bytes, NamedId::Bytes(vec![3, 4]));
491 kept.clone_from(&NamedId::I64(7));
493 assert_eq!(kept, NamedId::I64(7));
494 assert_eq!(kept.clone(), NamedId::I64(7));
495 let ids = [
496 NamedId::I16(1),
497 NamedId::I32(2),
498 NamedId::Text("job-3".to_owned()),
499 NamedId::Bytes(vec![4]),
500 ];
501 for id in ids {
502 assert_eq!(id.clone(), id);
503 }
504 }
505
506 #[test]
507 fn a_row_whose_columns_hold_its_structs_types_stays_a_row() -> Result<(), Error> {
508 let mut claimed = Claimed::Row(row::<()>(FITTING));
509 hold_to(KINDS, &mut claimed)?;
510 assert!(matches!(claimed, Claimed::Row(_)));
511 let mut keyless = Claimed::Row(row::<()>(Fits {
513 key: u8::MAX,
514 ..FITTING
515 }));
516 hold_to(KINDS, &mut keyless)?;
517 assert!(matches!(keyless, Claimed::Row(_)));
518 Ok(())
519 }
520
521 #[test]
522 fn a_column_that_does_not_hold_its_structs_type_sends_the_row_to_the_policy()
523 -> Result<(), Error> {
524 let text_payload = Fits {
525 payload: BytesKind::Text.bit(),
526 ..FITTING
527 };
528 let byte_key = Fits {
529 key: BytesKind::Bytes.bit(),
530 ..FITTING
531 };
532 let wide_attempt = Fits {
533 attempt: IntKind::I64.bit(),
534 ..FITTING
535 };
536 let text_payload_wide_attempt = Fits {
537 attempt: IntKind::I64.bit(),
538 ..text_payload
539 };
540 for (fits, column, attempt) in [
543 (text_payload, "\"payload\"", Some(2)),
544 (byte_key, "\"partition_key\"", Some(2)),
545 (wide_attempt, "\"attempt\"", None),
546 (text_payload_wide_attempt, "\"payload\"", None),
547 ] {
548 let mut claimed = Claimed::Row(row::<()>(fits));
549 hold_to(KINDS, &mut claimed)?;
550 assert_eq!(
551 unfit_column(&claimed),
552 Some((column.to_owned(), attempt)),
553 "{fits:?}"
554 );
555 }
556 Ok(())
557 }
558
559 #[test]
560 fn an_id_that_does_not_hold_its_structs_type_fails_the_claim() {
561 let mut claimed = Claimed::Row(row::<()>(Fits {
562 id: IdKind::I32.bit(),
563 ..FITTING
564 }));
565 let failed = hold_to(KINDS, &mut claimed);
566 assert!(
567 matches!(&failed, Err(Error::ColumnDecode { index, .. }) if index == "\"id\""),
568 "{failed:?}"
569 );
570 }
571
572 #[test]
573 fn a_row_already_undecodable_is_left_to_the_policy() -> Result<(), Error> {
574 let mut claimed = Claimed::<NamedRow<()>>::Undecodable {
575 id: NamedId::I64(7),
576 attempt: Some(3),
577 error: Box::new(undecodable(Error::ColumnNotFound("payload".to_owned()))),
578 };
579 hold_to(KINDS, &mut claimed)?;
580 assert!(matches!(
581 &claimed,
582 Claimed::Undecodable { attempt: Some(3), error, .. }
583 if matches!(driver_error(error), Some(Error::ColumnNotFound(_)))
584 ));
585 Ok(())
586 }
587}