use sqlx::{
FromRow, MySql, Pool, Postgres, Sqlite, mysql::MySqlRow, postgres::PgRow, sqlite::SqliteRow,
};
use std::marker::PhantomData;
use super::database_type::DatabaseType;
use super::mysql_reader::MySqlRdbcItemReader;
use super::postgres_reader::PostgresRdbcItemReader;
use super::select_builder::SelectBuilder;
use super::sqlite_reader::SqliteRdbcItemReader;
enum QuerySource<'a> {
Raw(&'a str),
Built(String),
}
pub struct RdbcItemReaderBuilder<'a, I> {
postgres_pool: Option<Pool<Postgres>>,
mysql_pool: Option<Pool<MySql>>,
sqlite_pool: Option<Pool<Sqlite>>,
query_source: Option<QuerySource<'a>>,
page_size: Option<i32>,
db_type: Option<DatabaseType>,
keyset_column: Option<String>,
#[allow(clippy::type_complexity)]
keyset_key_fn: Option<Box<dyn Fn(&I) -> String>>,
_phantom: PhantomData<I>,
}
impl<'a, I> RdbcItemReaderBuilder<'a, I> {
pub fn new() -> Self {
Self {
postgres_pool: None,
mysql_pool: None,
sqlite_pool: None,
query_source: None,
page_size: None,
db_type: None,
keyset_column: None,
keyset_key_fn: None,
_phantom: PhantomData,
}
}
pub fn postgres(mut self, pool: Pool<Postgres>) -> Self {
self.postgres_pool = Some(pool);
self.db_type = Some(DatabaseType::Postgres);
self
}
pub fn mysql(mut self, pool: Pool<MySql>) -> Self {
self.mysql_pool = Some(pool);
self.db_type = Some(DatabaseType::MySql);
self
}
pub fn sqlite(mut self, pool: Pool<Sqlite>) -> Self {
self.sqlite_pool = Some(pool);
self.db_type = Some(DatabaseType::Sqlite);
self
}
pub fn query(mut self, query: &'a str) -> Self {
self.query_source = Some(QuerySource::Raw(query));
self
}
pub fn select(mut self, builder: SelectBuilder<I>) -> Self {
let sql = if builder.keyset_column.is_some() {
builder.build_sql_no_order()
} else {
builder.build_sql()
};
if let Some(col) = builder.keyset_column {
self.keyset_column = Some(col);
}
if let Some(key_fn) = builder.keyset_key_fn {
self.keyset_key_fn = Some(key_fn);
}
self.query_source = Some(QuerySource::Built(sql));
self
}
pub fn with_page_size(mut self, page_size: i32) -> Self {
self.page_size = Some(page_size);
self
}
pub fn with_keyset(mut self, column: &str, key_fn: impl Fn(&I) -> String + 'static) -> Self {
self.keyset_column = Some(column.to_string());
self.keyset_key_fn = Some(Box::new(key_fn));
self
}
}
impl<'a, I> RdbcItemReaderBuilder<'a, I>
where
for<'r> I: FromRow<'r, PgRow> + Send + Unpin + Clone,
{
pub fn build_postgres(self) -> PostgresRdbcItemReader<I> {
let query = match self
.query_source
.expect("Query is required — call .query() or .select()")
{
QuerySource::Raw(s) => s.to_string(),
QuerySource::Built(s) => s,
};
PostgresRdbcItemReader::new(
self.postgres_pool.expect("PostgreSQL pool is required"),
query,
self.page_size,
self.keyset_column,
self.keyset_key_fn,
)
}
}
impl<'a, I> RdbcItemReaderBuilder<'a, I>
where
for<'r> I: FromRow<'r, MySqlRow> + Send + Unpin + Clone,
{
pub fn build_mysql(self) -> MySqlRdbcItemReader<I> {
let query = match self
.query_source
.expect("Query is required — call .query() or .select()")
{
QuerySource::Raw(s) => s.to_string(),
QuerySource::Built(s) => s,
};
MySqlRdbcItemReader::new(
self.mysql_pool.expect("MySQL pool is required"),
query,
self.page_size,
self.keyset_column,
self.keyset_key_fn,
)
}
}
impl<'a, I> RdbcItemReaderBuilder<'a, I>
where
for<'r> I: FromRow<'r, SqliteRow> + Send + Unpin + Clone,
{
pub fn build_sqlite(self) -> SqliteRdbcItemReader<I> {
let query = match self
.query_source
.expect("Query is required — call .query() or .select()")
{
QuerySource::Raw(s) => s.to_string(),
QuerySource::Built(s) => s,
};
SqliteRdbcItemReader::new(
self.sqlite_pool.expect("SQLite pool is required"),
query,
self.page_size,
self.keyset_column,
self.keyset_key_fn,
)
}
}
impl<'a, I> Default for RdbcItemReaderBuilder<'a, I> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::super::select_builder::SelectBuilder;
use super::*;
use sqlx::{FromRow, SqlitePool};
#[derive(Clone, FromRow)]
struct Dummy {
id: i32,
}
#[test]
fn should_create_via_default() {
let _b = RdbcItemReaderBuilder::<Dummy>::default();
}
#[test]
#[should_panic(expected = "SQLite pool is required")]
fn should_panic_when_building_sqlite_without_pool() {
let _ = RdbcItemReaderBuilder::<Dummy>::new()
.query("SELECT id FROM t")
.build_sqlite();
}
#[tokio::test(flavor = "multi_thread")]
async fn should_build_sqlite_reader_with_pool_and_query() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.query("SELECT 1 AS id")
.build_sqlite();
assert_eq!(reader.query, "SELECT 1 AS id");
assert_eq!(reader.page_size, None);
assert_eq!(reader.offset.get(), 0);
}
#[tokio::test(flavor = "multi_thread")]
async fn should_propagate_page_size_to_sqlite_reader() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.query("SELECT 1 AS id")
.with_page_size(25)
.build_sqlite();
assert_eq!(reader.page_size, Some(25));
}
#[test]
#[should_panic(expected = "PostgreSQL pool is required")]
fn should_panic_when_building_postgres_without_pool() {
let _ = RdbcItemReaderBuilder::<Dummy>::new()
.query("SELECT id FROM t")
.build_postgres();
}
#[test]
#[should_panic(expected = "MySQL pool is required")]
fn should_panic_when_building_mysql_without_pool() {
let _ = RdbcItemReaderBuilder::<Dummy>::new()
.query("SELECT id FROM t")
.build_mysql();
}
#[tokio::test(flavor = "multi_thread")]
async fn should_propagate_keyset_to_sqlite_reader() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.query("SELECT 1 AS id")
.with_page_size(5)
.with_keyset("id", |d: &Dummy| d.id.to_string())
.build_sqlite();
assert_eq!(
reader.keyset_column.as_deref(),
Some("id"),
"keyset column should be propagated to reader"
);
assert!(
reader.keyset_key.is_some(),
"keyset key fn should be propagated to reader"
);
assert!(
reader.last_cursor.borrow().is_none(),
"cursor starts as None"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn should_build_sqlite_reader_from_select_builder() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.select(
SelectBuilder::from("items")
.columns(&["id"])
.where_eq("active", true)
.order_by_asc("id"),
)
.build_sqlite();
assert_eq!(
reader.query, "SELECT id FROM items WHERE active = true ORDER BY id ASC",
"select builder SQL should be stored in reader"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn should_propagate_keyset_from_select_builder_to_sqlite_reader() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.select(
SelectBuilder::from("items").order_by_keyset("id", |d: &Dummy| d.id.to_string()),
)
.with_page_size(10)
.build_sqlite();
assert_eq!(
reader.keyset_column.as_deref(),
Some("id"),
"keyset column should propagate from SelectBuilder"
);
assert!(
reader.keyset_key.is_some(),
"keyset key fn should propagate from SelectBuilder"
);
assert_eq!(
reader.query, "SELECT * FROM items",
"keyset select builder must store SQL without ORDER BY to avoid double ORDER BY in read_page"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn should_prefer_select_over_query_when_called_last() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.query("SELECT id FROM old_table")
.select(SelectBuilder::from("new_table").columns(&["id"]))
.build_sqlite();
assert_eq!(
reader.query, "SELECT id FROM new_table",
"select() called last should win"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn should_prefer_query_over_select_when_called_last() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let reader = RdbcItemReaderBuilder::<Dummy>::new()
.sqlite(pool)
.select(SelectBuilder::from("old_table").columns(&["id"]))
.query("SELECT id FROM new_table")
.build_sqlite();
assert_eq!(
reader.query, "SELECT id FROM new_table",
"query() called last should win"
);
}
}