spring_batch_rs/item/rdbc/
postgres_writer.rs1use serde::Serialize;
2use sqlx::{Pool, Postgres, QueryBuilder};
3
4use crate::core::item::{ItemWriter, ItemWriterResult};
5use crate::item::rdbc::ColumnValue;
6
7use super::writer_common::{
8 bind_column_value, create_write_error, log_write_success, max_items_per_batch, validate_config,
9};
10
11pub struct PostgresItemWriter<O> {
44 pub(crate) pool: Option<sqlx::Pool<Postgres>>,
45 pub(crate) table: Option<String>,
46 #[allow(clippy::type_complexity)]
47 pub(crate) column_bindings: Vec<(String, Box<dyn Fn(&O) -> ColumnValue>)>,
48}
49
50impl<O> PostgresItemWriter<O> {
51 pub(crate) fn new() -> Self {
53 Self {
54 pool: None,
55 table: None,
56 column_bindings: Vec::new(),
57 }
58 }
59
60 pub(crate) fn pool(mut self, pool: &Pool<Postgres>) -> Self {
62 self.pool = Some(pool.clone());
63 self
64 }
65
66 pub(crate) fn table(mut self, table: &str) -> Self {
68 self.table = Some(table.to_string());
69 self
70 }
71
72 pub(crate) fn add_column_binding(
74 mut self,
75 name: String,
76 extractor: Box<dyn Fn(&O) -> ColumnValue>,
77 ) -> Self {
78 self.column_bindings.push((name, extractor));
79 self
80 }
81}
82
83impl<O> Default for PostgresItemWriter<O> {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl<O: Serialize + Clone> ItemWriter<O> for PostgresItemWriter<O> {
90 fn write(&self, items: &[O]) -> ItemWriterResult {
91 if items.is_empty() {
92 return Ok(());
93 }
94
95 let (pool, table) = validate_config(
96 self.pool.as_ref(),
97 self.table.as_deref(),
98 self.column_bindings.len(),
99 )?;
100
101 let col_names: Vec<&str> = self
102 .column_bindings
103 .iter()
104 .map(|(n, _)| n.as_str())
105 .collect();
106
107 let col_list = col_names.join(",");
108 let max_items = max_items_per_batch(self.column_bindings.len());
109
110 for chunk in items.chunks(max_items) {
111 let mut query_builder = QueryBuilder::new("INSERT INTO ");
112 query_builder.push(table);
113 query_builder.push(" (");
114 query_builder.push(&col_list);
115 query_builder.push(") ");
116
117 query_builder.push_values(chunk.iter(), |mut b, item| {
118 for (_, extractor) in &self.column_bindings {
119 bind_column_value!(b, extractor(item));
120 }
121 });
122
123 let query = query_builder.build();
124 let result = tokio::task::block_in_place(|| {
125 tokio::runtime::Handle::current().block_on(async { query.execute(pool).await })
126 });
127
128 if let Err(e) = result {
129 return Err(create_write_error(table, "PostgreSQL", e));
130 }
131 }
132
133 log_write_success(items.len(), table, "PostgreSQL");
134 Ok(())
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::item::rdbc::ColumnValue;
142
143 #[test]
144 fn should_start_with_empty_state() {
145 let writer = PostgresItemWriter::<String>::new();
146 assert!(writer.pool.is_none());
147 assert!(writer.table.is_none());
148 assert!(writer.column_bindings.is_empty());
149 }
150
151 #[test]
152 fn should_store_column_bindings_in_order() {
153 let writer = PostgresItemWriter::<String>::new()
154 .add_column_binding("x".to_string(), Box::new(|_| ColumnValue::Null))
155 .add_column_binding("y".to_string(), Box::new(|_| ColumnValue::Null));
156 let names: Vec<&str> = writer
157 .column_bindings
158 .iter()
159 .map(|(n, _)| n.as_str())
160 .collect();
161 assert_eq!(names, vec!["x", "y"]);
162 }
163
164 #[test]
165 fn should_return_ok_for_empty_items() {
166 let writer = PostgresItemWriter::<String>::new();
167 assert!(writer.write(&[]).is_ok());
168 }
169
170 #[test]
171 fn should_return_error_when_no_columns_and_items_given() {
172 use crate::BatchError;
173 let writer = PostgresItemWriter::<String>::new().table("t");
174 let result = writer.write(&["x".to_string()]);
175 match result.err().unwrap() {
176 BatchError::ItemWriter(msg) => assert!(msg.contains("columns"), "{msg}"),
177 e => panic!("expected ItemWriter, got {e:?}"),
178 }
179 }
180
181 #[test]
182 fn should_return_error_when_pool_not_configured() {
183 use crate::BatchError;
184 let writer = PostgresItemWriter::<String>::new()
185 .table("t")
186 .add_column_binding("v".to_string(), Box::new(|s: &String| s.as_str().into()));
187 let result = writer.write(&["x".to_string()]);
188 match result.err().unwrap() {
189 BatchError::ItemWriter(msg) => assert!(msg.contains("pool"), "{msg}"),
190 e => panic!("expected ItemWriter, got {e:?}"),
191 }
192 }
193}