use std::future::Future;
use std::pin::Pin;
use crate::error::DbError;
use crate::pool::{Connection, QueryRows};
use crate::value::Value;
#[cfg(feature = "prepared-stmt-cache")]
use crate::prepared_cache::{ConnId, PreparedLookup, PreparedStatementCache};
#[cfg(feature = "async-row-stream")]
use crate::row_stream::{AsyncRowStream, BoxedCursorRowStream};
pub trait ConnectionExt: Connection {
#[cfg(feature = "prepared-stmt-cache")]
fn conn_id(&self) -> ConnId {
0
}
#[cfg(feature = "prepared-stmt-cache")]
fn prepare_cached<'a>(
&'a mut self,
cache: &'a PreparedStatementCache,
sql: &'a str,
params: &'a [Value],
) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
Box::pin(async move {
match cache.get_or_prepare(self.conn_id(), sql, params).await? {
PreparedLookup::Hit(rows) => Ok(rows),
PreparedLookup::Miss => {
let rows = self.query_with_params(sql, params).await?;
Ok(rows)
}
}
})
}
#[cfg(feature = "async-row-stream")]
fn query_stream_unified<'a>(
&'a mut self,
sql: &'a str,
batch_size: usize,
) -> Result<Box<dyn AsyncRowStream + 'a>, DbError> {
if batch_size == 0 {
return Err(DbError::InvalidInput("batch_size 必须大于 0".to_string()));
}
let stream = self.query_stream_cursor(sql, batch_size);
Ok(Box::new(BoxedCursorRowStream::new(stream)))
}
fn execute_batch_parallel<'a>(
&'a mut self,
sqls: &'a [String],
concurrency: usize,
) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
let _ = concurrency;
self.execute_batch(sqls)
}
#[cfg(feature = "query-result-cache")]
fn query_with_result_cache<'a>(
&'a mut self,
cache: &'a crate::query_result_cache::QueryResultCache,
sql: &'a str,
params: &'a [Value],
depends_on: std::collections::HashSet<String>,
) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
Box::pin(async move {
let key = crate::query_result_cache::CacheKey::new(sql, params, None);
if let Some(rows) = cache.get(&key) {
return Ok(rows);
}
let rows = self.query_with_params(sql, params).await?;
cache.put(key, rows.clone(), depends_on);
Ok(rows)
})
}
}
impl<C: Connection + ?Sized> ConnectionExt for C {}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
struct MockConnection {
connected: bool,
}
impl MockConnection {
fn new() -> Self {
Self { connected: true }
}
}
impl Connection for MockConnection {
fn execute<'a>(
&'a mut self,
sql: &'a str,
) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
let _ = sql;
Box::pin(async { Ok(1) })
}
fn query<'a>(
&'a mut self,
sql: &'a str,
) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
let _ = sql;
Box::pin(async { Ok(vec![HashMap::new()]) })
}
fn begin_transaction<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn commit<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn rollback<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn is_connected(&self) -> bool {
self.connected
}
fn ping<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
Box::pin(async { true })
}
fn close<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
self.connected = false;
Box::pin(async { Ok(()) })
}
fn query_with_params<'a>(
&'a mut self,
sql: &'a str,
params: &'a [Value],
) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
let _ = (sql, params);
Box::pin(async { Ok(vec![HashMap::new()]) })
}
}
#[tokio::test]
async fn test_prepare_cached_default() {
#[cfg(feature = "prepared-stmt-cache")]
{
let mut conn = MockConnection::new();
let cache = PreparedStatementCache::new(256);
let rows = conn
.prepare_cached(&cache, "SELECT 1", &[Value::I64(1)])
.await
.unwrap();
assert_eq!(rows.len(), 1);
}
}
#[tokio::test]
async fn test_execute_batch_parallel_default() {
let mut conn = MockConnection::new();
let sqls = vec!["INSERT INTO t VALUES (1)".to_string()];
let count = conn.execute_batch_parallel(&sqls, 0).await.unwrap();
assert_eq!(count, 1);
}
#[tokio::test]
async fn test_execute_batch_parallel_concurrency() {
let mut conn = MockConnection::new();
let sqls = vec![
"INSERT INTO t VALUES (1)".to_string(),
"INSERT INTO t VALUES (2)".to_string(),
];
let count = conn.execute_batch_parallel(&sqls, 2).await.unwrap();
assert_eq!(count, 2);
}
#[tokio::test]
async fn test_execute_batch_parallel_empty() {
let mut conn = MockConnection::new();
let sqls: Vec<String> = vec![];
let count = conn.execute_batch_parallel(&sqls, 4).await.unwrap();
assert_eq!(count, 0);
}
#[cfg(feature = "async-row-stream")]
#[tokio::test]
async fn test_query_stream_unified_default() {
use crate::row_stream::AsyncRowStream;
let mut conn = MockConnection::new();
let mut stream = conn.query_stream_unified("SELECT 1", 100).unwrap();
let row = stream.next_row().await;
assert!(row.is_some());
}
#[cfg(feature = "async-row-stream")]
#[test]
fn test_query_stream_unified_zero_batch() {
let mut conn = MockConnection::new();
let result = conn.query_stream_unified("SELECT 1", 0);
assert!(result.is_err());
}
#[cfg(feature = "query-result-cache")]
#[tokio::test]
async fn test_query_with_result_cache_miss_then_hit() {
use crate::query_result_cache::QueryResultCache;
use std::collections::HashSet;
let mut conn = MockConnection::new();
let cache = QueryResultCache::with_default();
let deps = HashSet::new();
let rows1 = conn
.query_with_result_cache(&cache, "SELECT 1", &[], deps.clone())
.await
.unwrap();
assert_eq!(rows1.len(), 1);
let rows2 = conn
.query_with_result_cache(&cache, "SELECT 1", &[], deps)
.await
.unwrap();
assert_eq!(rows2.len(), 1);
let stats = cache.stats();
assert!(stats.hits >= 1);
assert!(stats.misses >= 1);
}
#[cfg(feature = "query-result-cache")]
#[tokio::test]
async fn test_query_with_result_cache_invalidate() {
use crate::query_result_cache::QueryResultCache;
use std::collections::HashSet;
let mut conn = MockConnection::new();
let cache = QueryResultCache::with_default();
let mut deps = HashSet::new();
deps.insert("users".to_string());
let _ = conn
.query_with_result_cache(&cache, "SELECT * FROM users", &[], deps)
.await
.unwrap();
let key = crate::query_result_cache::CacheKey::new("SELECT * FROM users", &[], None);
assert!(cache.get(&key).is_some());
let count = cache.invalidate_table("users");
assert_eq!(count, 1);
assert!(cache.get(&key).is_none());
}
#[cfg(feature = "query-result-cache")]
#[tokio::test]
async fn test_query_with_result_cache_different_params() {
use crate::query_result_cache::QueryResultCache;
use std::collections::HashSet;
let mut conn = MockConnection::new();
let cache = QueryResultCache::with_default();
let deps = HashSet::new();
let _ = conn
.query_with_result_cache(
&cache,
"SELECT * WHERE id = ?",
&[Value::I64(1)],
deps.clone(),
)
.await
.unwrap();
let _ = conn
.query_with_result_cache(&cache, "SELECT * WHERE id = ?", &[Value::I64(2)], deps)
.await
.unwrap();
let stats = cache.stats();
assert!(stats.misses >= 2);
}
#[cfg(feature = "query-result-cache")]
#[tokio::test]
async fn test_query_with_result_cache_cache_key_tenant() {
use crate::query_result_cache::{CacheKey, QueryResultCache};
use std::collections::HashSet;
let cache = QueryResultCache::with_default();
let k1 = CacheKey::new("SELECT 1", &[], Some(1));
let k2 = CacheKey::new("SELECT 1", &[], Some(2));
let rows = vec![HashMap::new()];
cache.put(k1.clone(), rows.clone(), HashSet::new());
cache.put(k2.clone(), rows, HashSet::new());
assert!(cache.get(&k1).is_some());
assert!(cache.get(&k2).is_some());
let stats = cache.stats();
assert_eq!(stats.hits, 2);
}
#[cfg(feature = "query-result-cache")]
#[tokio::test]
async fn test_query_with_result_cache_stats() {
use crate::query_result_cache::QueryResultCache;
use std::collections::HashSet;
let mut conn = MockConnection::new();
let cache = QueryResultCache::with_default();
let deps = HashSet::new();
for _ in 0..3 {
let _ = conn
.query_with_result_cache(&cache, "SELECT 1", &[], deps.clone())
.await
.unwrap();
}
let stats = cache.stats();
assert!(stats.hits >= 2);
assert!(stats.misses >= 1);
assert!(stats.hit_rate() > 0.0);
}
#[cfg(all(feature = "query-result-cache", feature = "prepared-stmt-cache"))]
#[tokio::test]
async fn test_query_with_result_cache_and_prepared_cache_coexist() {
use crate::prepared_cache::PreparedStatementCache;
use crate::query_result_cache::QueryResultCache;
use std::collections::HashSet;
let mut conn = MockConnection::new();
let result_cache = QueryResultCache::with_default();
let stmt_cache = PreparedStatementCache::new(256);
let deps = HashSet::new();
let rows1 = conn
.query_with_result_cache(&result_cache, "SELECT 1", &[], deps.clone())
.await
.unwrap();
assert_eq!(rows1.len(), 1);
let rows2 = conn
.prepare_cached(&stmt_cache, "SELECT 1", &[Value::I64(1)])
.await
.unwrap();
assert_eq!(rows2.len(), 1);
let stats = result_cache.stats();
assert!(stats.total_queries() >= 1);
}
}