use sqlx::{MySql, Pool, Postgres, Sqlite};
use super::column_value::ColumnValue;
use super::mysql_writer::MySqlItemWriter;
use super::postgres_writer::PostgresItemWriter;
use super::sqlite_writer::SqliteItemWriter;
pub struct RdbcItemWriterBuilder<O> {
postgres_pool: Option<sqlx::Pool<Postgres>>,
mysql_pool: Option<sqlx::Pool<MySql>>,
sqlite_pool: Option<sqlx::Pool<Sqlite>>,
table: Option<String>,
#[allow(clippy::type_complexity)]
column_bindings: Vec<(String, Box<dyn Fn(&O) -> ColumnValue>)>,
}
impl<O> RdbcItemWriterBuilder<O> {
pub fn new() -> Self {
Self {
postgres_pool: None,
mysql_pool: None,
sqlite_pool: None,
table: None,
column_bindings: Vec::new(),
}
}
pub fn postgres(mut self, pool: &Pool<Postgres>) -> Self {
self.postgres_pool = Some(pool.clone());
self
}
pub fn mysql(mut self, pool: &Pool<MySql>) -> Self {
self.mysql_pool = Some(pool.clone());
self
}
pub fn sqlite(mut self, pool: &Pool<Sqlite>) -> Self {
self.sqlite_pool = Some(pool.clone());
self
}
pub fn table(mut self, table: &str) -> Self {
self.table = Some(table.to_string());
self
}
pub fn column(mut self, name: &str, extractor: impl Fn(&O) -> ColumnValue + 'static) -> Self {
self.column_bindings
.push((name.to_string(), Box::new(extractor)));
self
}
pub fn build_postgres(self) -> PostgresItemWriter<O> {
let mut writer = PostgresItemWriter::new();
if let Some(pool) = self.postgres_pool {
writer = writer.pool(&pool);
}
if let Some(table) = self.table {
writer = writer.table(&table);
}
for (name, extractor) in self.column_bindings {
writer = writer.add_column_binding(name, extractor);
}
writer
}
pub fn build_mysql(self) -> MySqlItemWriter<O> {
let mut writer = MySqlItemWriter::new();
if let Some(pool) = self.mysql_pool {
writer = writer.pool(&pool);
}
if let Some(table) = self.table {
writer = writer.table(&table);
}
for (name, extractor) in self.column_bindings {
writer = writer.add_column_binding(name, extractor);
}
writer
}
pub fn build_sqlite(self) -> SqliteItemWriter<O> {
let mut writer = SqliteItemWriter::new();
if let Some(pool) = self.sqlite_pool {
writer = writer.pool(&pool);
}
if let Some(table) = self.table {
writer = writer.table(&table);
}
for (name, extractor) in self.column_bindings {
writer = writer.add_column_binding(name, extractor);
}
writer
}
}
impl<O> Default for RdbcItemWriterBuilder<O> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::item::ItemWriter;
use crate::item::rdbc::ColumnValue;
struct User {
id: i32,
name: String,
}
#[test]
fn should_accumulate_columns_in_order() {
let writer = RdbcItemWriterBuilder::<User>::new()
.column("id", |u: &User| u.id.into())
.column("name", |u: &User| u.name.as_str().into())
.build_postgres();
let names: Vec<&str> = writer
.column_bindings
.iter()
.map(|(n, _)| n.as_str())
.collect();
assert_eq!(
names,
vec!["id", "name"],
"columns must be in insertion order"
);
}
#[test]
fn should_set_table_in_postgres_writer() {
let writer = RdbcItemWriterBuilder::<String>::new()
.table("users")
.build_postgres();
assert_eq!(
writer.table.as_deref(),
Some("users"),
"table name must be transferred to postgres writer"
);
}
#[test]
fn should_set_table_in_mysql_writer() {
let writer = RdbcItemWriterBuilder::<String>::new()
.table("products")
.build_mysql();
assert_eq!(
writer.table.as_deref(),
Some("products"),
"table name must be transferred to mysql writer"
);
}
#[test]
fn should_set_table_in_sqlite_writer() {
use crate::BatchError;
let writer = RdbcItemWriterBuilder::<String>::new()
.table("items")
.column("sku", |s: &String| s.as_str().into())
.build_sqlite();
let result = writer.write(&["x".to_string()]);
match result.err().unwrap() {
BatchError::ItemWriter(msg) => assert!(
msg.contains("pool"),
"table and columns were set, so error should be about pool, got: {msg}"
),
e => panic!("expected ItemWriter, got {e:?}"),
}
}
#[test]
fn should_build_via_default() {
let builder = RdbcItemWriterBuilder::<String>::default();
let writer = builder.build_postgres();
assert!(
writer.table.is_none(),
"default builder should have no table"
);
assert!(
writer.column_bindings.is_empty(),
"default builder should have no column bindings"
);
}
#[test]
fn should_transfer_columns_to_mysql_writer() {
let writer = RdbcItemWriterBuilder::<String>::new()
.column("a", |s: &String| s.as_str().into())
.column("b", |_: &String| ColumnValue::Null)
.build_mysql();
let names: Vec<&str> = writer
.column_bindings
.iter()
.map(|(n, _)| n.as_str())
.collect();
assert_eq!(
names,
vec!["a", "b"],
"columns must reach mysql writer in order"
);
}
#[test]
fn should_transfer_columns_to_sqlite_writer() {
let writer = RdbcItemWriterBuilder::<String>::new()
.column("x", |_: &String| ColumnValue::Null)
.build_sqlite();
assert_eq!(
writer.column_bindings.len(),
1,
"one column binding should be transferred to sqlite writer"
);
assert_eq!(writer.column_bindings[0].0, "x");
}
#[test]
fn should_have_no_pool_by_default_in_postgres_writer() {
let writer = RdbcItemWriterBuilder::<String>::new().build_postgres();
assert!(writer.pool.is_none(), "pool should be None when not set");
}
#[test]
fn should_have_no_pool_by_default_in_mysql_writer() {
let writer = RdbcItemWriterBuilder::<String>::new().build_mysql();
assert!(writer.pool.is_none(), "pool should be None when not set");
}
#[tokio::test(flavor = "multi_thread")]
async fn should_transfer_pool_to_sqlite_writer() {
use crate::BatchError;
let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
let writer = RdbcItemWriterBuilder::<String>::new()
.sqlite(&pool)
.table("t")
.column("v", |s: &String| s.as_str().into())
.build_sqlite();
let result = writer.write(&["x".to_string()]);
match result.err().unwrap() {
BatchError::ItemWriter(msg) => assert!(
msg.contains("SQLite"),
"pool transferred — should get SQLite DB error, got: {msg}"
),
e => panic!("expected ItemWriter, got {e:?}"),
}
}
}