spring_batch_rs/item/rdbc/
mysql_reader.rs1use std::cell::{Cell, RefCell};
2
3use sqlx::{FromRow, MySql, Pool, QueryBuilder, mysql::MySqlRow};
4
5use super::reader_common::read_item;
6use crate::BatchError;
7use crate::core::item::{ItemReader, ItemReaderResult};
8
9pub struct MySqlRdbcItemReader<I>
18where
19 for<'r> I: FromRow<'r, MySqlRow> + Send + Unpin + Clone,
20{
21 pub(crate) pool: Pool<MySql>,
22 pub(crate) query: String,
23 pub(crate) page_size: Option<i32>,
24 pub(crate) offset: Cell<i32>,
25 pub(crate) buffer: RefCell<Vec<I>>,
26 pub(crate) keyset_column: Option<String>,
27 #[allow(clippy::type_complexity)]
28 pub(crate) keyset_key: Option<Box<dyn Fn(&I) -> String>>,
29 pub(crate) last_cursor: RefCell<Option<String>>,
30}
31
32impl<I> MySqlRdbcItemReader<I>
33where
34 for<'r> I: FromRow<'r, MySqlRow> + Send + Unpin + Clone,
35{
36 #[allow(clippy::type_complexity)]
41 pub fn new(
42 pool: Pool<MySql>,
43 query: String,
44 page_size: Option<i32>,
45 keyset_column: Option<String>,
46 keyset_key: Option<Box<dyn Fn(&I) -> String>>,
47 ) -> Self {
48 Self {
49 pool,
50 query,
51 page_size,
52 offset: Cell::new(0),
53 buffer: RefCell::new(vec![]),
54 keyset_column,
55 keyset_key,
56 last_cursor: RefCell::new(None),
57 }
58 }
59
60 fn read_page(&self) -> Result<(), BatchError> {
66 let mut query_builder = QueryBuilder::<MySql>::new(&self.query);
67
68 if let Some(page_size) = self.page_size {
69 if let Some(ref col) = self.keyset_column {
70 let last = self.last_cursor.borrow();
71 if let Some(ref cursor_val) = *last {
72 let escaped = cursor_val.replace('\'', "''");
73 query_builder.push(format!(" WHERE {} > '{}'", col, escaped));
74 }
75 drop(last);
76 query_builder.push(format!(" ORDER BY {} LIMIT {}", col, page_size));
77 } else {
78 query_builder.push(format!(" LIMIT {} OFFSET {}", page_size, self.offset.get()));
79 }
80 }
81
82 let query = query_builder.build_query_as::<I>();
83 let items = tokio::task::block_in_place(|| {
84 tokio::runtime::Handle::current().block_on(async {
85 query
86 .fetch_all(&self.pool)
87 .await
88 .map_err(|e| BatchError::ItemReader(e.to_string()))
89 })
90 })?;
91
92 *self.buffer.borrow_mut() = items;
93 Ok(())
94 }
95}
96
97impl<I> ItemReader<I> for MySqlRdbcItemReader<I>
98where
99 for<'r> I: FromRow<'r, MySqlRow> + Send + Unpin + Clone,
100{
101 fn read(&self) -> ItemReaderResult<I> {
102 read_item(
103 &self.offset,
104 self.page_size,
105 &self.buffer,
106 &self.keyset_key,
107 &self.last_cursor,
108 || self.read_page(),
109 )
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use sqlx::MySqlPool;
117
118 #[derive(Clone)]
119 struct Dummy;
120
121 impl<'r> sqlx::FromRow<'r, sqlx::mysql::MySqlRow> for Dummy {
122 fn from_row(_row: &'r sqlx::mysql::MySqlRow) -> Result<Self, sqlx::Error> {
123 Ok(Dummy)
124 }
125 }
126
127 fn reader_with_keyset(keyset: bool) -> MySqlRdbcItemReader<Dummy> {
128 let pool = MySqlPool::connect_lazy("mysql://root:root@localhost/test")
129 .expect("lazy pool creation should not fail");
130 let (col, key): (Option<String>, Option<Box<dyn Fn(&Dummy) -> String>>) = if keyset {
131 (
132 Some("id".to_string()),
133 Some(Box::new(|_: &Dummy| "0".to_string())),
134 )
135 } else {
136 (None, None)
137 };
138 MySqlRdbcItemReader::new(pool, "SELECT 1".to_string(), Some(10), col, key)
139 }
140
141 #[tokio::test(flavor = "multi_thread")]
142 async fn should_initialize_without_keyset() {
143 let reader = reader_with_keyset(false);
144 assert!(reader.keyset_column.is_none(), "no keyset column expected");
145 assert!(reader.keyset_key.is_none(), "no keyset key fn expected");
146 assert!(
147 reader.last_cursor.borrow().is_none(),
148 "cursor must start as None"
149 );
150 assert_eq!(reader.offset.get(), 0, "initial offset should be 0");
151 assert!(
152 reader.buffer.borrow().is_empty(),
153 "buffer should start empty"
154 );
155 assert_eq!(reader.page_size, Some(10));
156 }
157
158 #[tokio::test(flavor = "multi_thread")]
159 async fn should_initialize_with_keyset_column_and_none_cursor() {
160 let reader = reader_with_keyset(true);
161 assert_eq!(
162 reader.keyset_column.as_deref(),
163 Some("id"),
164 "keyset column should be stored"
165 );
166 assert!(
167 reader.keyset_key.is_some(),
168 "keyset key fn should be stored"
169 );
170 assert!(
171 reader.last_cursor.borrow().is_none(),
172 "cursor must start as None before first read"
173 );
174 }
175}