use anyhow::Result;
use futures::TryStreamExt;
use tokio::pin;
use crate::database::scylla::client::Client;
pub const MAX_BLOB_SIZE: usize = 512000;
pub const TABLE_NAME: &str = "blobs";
pub const COLUMNS: &str = "key, position, data";
pub const MAX_PAGES_PER_SELECT: i32 = 1000;
lazy_static! {
static ref INSERT_STATEMENT: String = format!(
"INSERT INTO :KEYSPACE:.{} ({}) VALUES (?, ?, ?)",
TABLE_NAME, COLUMNS
);
static ref SELECT_STATEMENT: String = format!(
"SELECT {} FROM :KEYSPACE:.{} WHERE key = ?",
COLUMNS, TABLE_NAME
);
static ref DELETE_KEY_PREFIX_STATEMENT: String = format!(
"DELETE FROM :KEYSPACE:.{} WHERE key = ?",
TABLE_NAME
);
}
type TypedBlobRow = (String, i64, Vec<u8>);
pub struct BlobTable;
impl BlobTable {
pub async fn insert(client: &Client, key: &str, data: &[u8]) -> Result<()> {
let prepared_statement = client.get_prepared_statement(&INSERT_STATEMENT).await?;
for (i, chunk) in data.chunks(MAX_BLOB_SIZE).enumerate() {
client
.execute_unpaged(&prepared_statement, (key, i as i64, Vec::from(chunk)))
.await?;
}
Ok(())
}
pub async fn select(client: &Client, key: &str) -> Result<Vec<u8>> {
let prepared_statement = client.get_prepared_statement(&SELECT_STATEMENT).await?;
let stream = client
.execute_iter(prepared_statement, (key,))
.await?
.rows_stream::<TypedBlobRow>()?;
pin!(stream);
let mut chunks = stream.try_collect::<Vec<_>>().await?;
chunks.sort_by(|a, b| a.1.cmp(&b.1));
Ok(chunks.into_iter().flat_map(|chunk| chunk.2).collect())
}
pub async fn delete(client: &Client, key: &str) -> Result<()> {
let prepared_statement = client
.get_prepared_statement(&DELETE_KEY_PREFIX_STATEMENT)
.await?;
client.execute_unpaged(&prepared_statement, (key,)).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use lipsum::lipsum;
use serial_test::serial;
use super::*;
use crate::database::generic_client::GenericClient;
use crate::database::scylla::prepare_database_for_tests;
use crate::database::scylla::tests::get_test_database_url;
#[tokio::test]
#[serial]
async fn test_insert_select_delete() {
let client = Client::new(&get_test_database_url()).await.unwrap();
prepare_database_for_tests(&client).await;
let min_length = MAX_BLOB_SIZE * 4;
let mut lorem_ipsum = String::with_capacity(min_length);
while lorem_ipsum.len() < min_length {
lorem_ipsum.push_str(lipsum(20).as_str());
}
let expected_num_blobs = (lorem_ipsum.len() as f64 / MAX_BLOB_SIZE as f64).ceil() as usize;
BlobTable::insert(&client, "lorem_ipsum", lorem_ipsum.as_bytes())
.await
.unwrap();
let prepared_count_statement = client
.prepare(format!(
"SELECT count(*) FROM {}.{} WHERE key = ?",
client.get_database(),
TABLE_NAME
))
.await
.unwrap();
let res = client
.execute_unpaged(&prepared_count_statement, ("lorem_ipsum",))
.await
.unwrap()
.into_rows_result()
.unwrap();
assert_eq!(res.rows_num(), 1);
assert_eq!(
res.first_row::<(i64,)>().unwrap(),
(expected_num_blobs as i64,)
);
let selected_lorem_ipsum_bytes = BlobTable::select(&client, "lorem_ipsum").await.unwrap();
assert_eq!(selected_lorem_ipsum_bytes.len(), lorem_ipsum.len(),);
assert_eq!(
String::from_utf8(selected_lorem_ipsum_bytes)
.unwrap()
.to_string(),
lorem_ipsum
);
BlobTable::delete(&client, "lorem_ipsum").await.unwrap();
let res = client
.execute_unpaged(&prepared_count_statement, ("lorem_ipsum",))
.await
.unwrap()
.into_rows_result()
.unwrap();
assert_eq!(res.rows_num(), 1);
assert_eq!(res.first_row::<(i64,)>().unwrap(), (0,));
}
#[tokio::test]
#[serial]
async fn test_tokenawareness() {
let client = Client::new(&get_test_database_url()).await.unwrap();
prepare_database_for_tests(&client).await;
for statement in &[
INSERT_STATEMENT.as_str(),
SELECT_STATEMENT.as_str(),
DELETE_KEY_PREFIX_STATEMENT.as_str(),
] {
let prepared_statement = client.get_prepared_statement(statement).await.unwrap();
assert!(prepared_statement.is_token_aware());
}
}
}