use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableType {
#[default]
Unlogged,
Regular,
}
impl TableType {
#[inline]
pub fn sql_keyword(&self) -> &'static str {
match self {
TableType::Unlogged => "UNLOGGED",
TableType::Regular => "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TtlCleanupStrategy {
#[default]
OnRead,
Manual,
Disabled,
}
#[derive(Debug, Clone)]
pub struct Config {
pub(crate) connection_string: String,
pub(crate) table_name: String,
pub(crate) table_type: TableType,
pub(crate) auto_create_table: bool,
pub(crate) ttl_cleanup_strategy: TtlCleanupStrategy,
pub(crate) max_key_length: usize,
pub(crate) max_value_size: usize,
pub(crate) schema: Option<String>,
pub(crate) connect_timeout_secs: u64,
pub(crate) application_name: Option<String>,
}
impl Default for Config {
fn default() -> Self {
Self {
connection_string: String::new(),
table_name: "kv_store".to_string(),
table_type: TableType::Unlogged,
auto_create_table: true,
ttl_cleanup_strategy: TtlCleanupStrategy::OnRead,
max_key_length: 1024, max_value_size: 100 * 1024 * 1024, schema: None,
connect_timeout_secs: 10,
application_name: None,
}
}
}
impl Config {
pub fn new(connection_string: impl Into<String>) -> Self {
Self {
connection_string: connection_string.into(),
..Default::default()
}
}
pub fn table_name(mut self, name: impl Into<String>) -> Self {
self.table_name = name.into();
self
}
pub fn table_type(mut self, table_type: TableType) -> Self {
self.table_type = table_type;
self
}
pub fn auto_create_table(mut self, auto_create: bool) -> Self {
self.auto_create_table = auto_create;
self
}
pub fn ttl_cleanup_strategy(mut self, strategy: TtlCleanupStrategy) -> Self {
self.ttl_cleanup_strategy = strategy;
self
}
pub fn max_key_length(mut self, length: usize) -> Self {
self.max_key_length = length;
self
}
pub fn max_value_size(mut self, size: usize) -> Self {
self.max_value_size = size;
self
}
pub fn schema(mut self, schema: impl Into<String>) -> Self {
self.schema = Some(schema.into());
self
}
pub fn connect_timeout(mut self, secs: u64) -> Self {
self.connect_timeout_secs = secs;
self
}
pub fn application_name(mut self, name: impl Into<String>) -> Self {
self.application_name = Some(name.into());
self
}
pub(crate) fn qualified_table_name(&self) -> String {
match &self.schema {
Some(schema) => format!("{}.{}", quote_ident(schema), quote_ident(&self.table_name)),
None => quote_ident(&self.table_name),
}
}
#[inline]
pub(crate) fn ttl_enabled(&self) -> bool {
self.ttl_cleanup_strategy != TtlCleanupStrategy::Disabled
}
#[inline]
pub(crate) fn cleanup_on_read(&self) -> bool {
self.ttl_cleanup_strategy == TtlCleanupStrategy::OnRead
}
pub(crate) fn validate(&self) -> Result<()> {
if self.connection_string.is_empty() {
return Err(Error::Config("connection string cannot be empty".into()));
}
if self.table_name.is_empty() {
return Err(Error::Config("table name cannot be empty".into()));
}
if self.table_name.len() > 63 {
return Err(Error::Config(
"table name exceeds PostgreSQL's 63 character limit".into(),
));
}
if self.max_key_length == 0 {
return Err(Error::Config(
"max_key_length must be greater than 0".into(),
));
}
if self.max_value_size == 0 {
return Err(Error::Config(
"max_value_size must be greater than 0".into(),
));
}
Ok(())
}
}
fn quote_ident(ident: &str) -> String {
let escaped = ident.replace('"', "\"\"");
format!("\"{}\"", escaped)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::new("postgresql://localhost/test");
assert_eq!(config.table_name, "kv_store");
assert_eq!(config.table_type, TableType::Unlogged);
assert!(config.auto_create_table);
assert_eq!(config.ttl_cleanup_strategy, TtlCleanupStrategy::OnRead);
assert!(config.ttl_enabled());
assert!(config.cleanup_on_read());
}
#[test]
fn test_config_builder() {
let config = Config::new("postgresql://localhost/test")
.table_name("custom_table")
.table_type(TableType::Regular)
.auto_create_table(false)
.ttl_cleanup_strategy(TtlCleanupStrategy::Manual)
.max_key_length(2048)
.max_value_size(1024)
.schema("custom_schema")
.application_name("my_app");
assert_eq!(config.table_name, "custom_table");
assert_eq!(config.table_type, TableType::Regular);
assert!(!config.auto_create_table);
assert_eq!(config.ttl_cleanup_strategy, TtlCleanupStrategy::Manual);
assert!(config.ttl_enabled());
assert!(!config.cleanup_on_read());
assert_eq!(config.max_key_length, 2048);
assert_eq!(config.max_value_size, 1024);
assert_eq!(config.schema, Some("custom_schema".to_string()));
assert_eq!(config.application_name, Some("my_app".to_string()));
}
#[test]
fn test_ttl_cleanup_strategies() {
let config = Config::new("postgresql://localhost/test")
.ttl_cleanup_strategy(TtlCleanupStrategy::OnRead);
assert!(config.ttl_enabled());
assert!(config.cleanup_on_read());
let config = Config::new("postgresql://localhost/test")
.ttl_cleanup_strategy(TtlCleanupStrategy::Manual);
assert!(config.ttl_enabled());
assert!(!config.cleanup_on_read());
let config = Config::new("postgresql://localhost/test")
.ttl_cleanup_strategy(TtlCleanupStrategy::Disabled);
assert!(!config.ttl_enabled());
assert!(!config.cleanup_on_read());
}
#[test]
fn test_qualified_table_name() {
let config = Config::new("postgresql://localhost/test").table_name("my_table");
assert_eq!(config.qualified_table_name(), "\"my_table\"");
let config = Config::new("postgresql://localhost/test")
.table_name("my_table")
.schema("my_schema");
assert_eq!(config.qualified_table_name(), "\"my_schema\".\"my_table\"");
}
#[test]
fn test_quote_ident() {
assert_eq!(quote_ident("simple"), "\"simple\"");
assert_eq!(quote_ident("has\"quote"), "\"has\"\"quote\"");
}
#[test]
fn test_table_type_sql() {
assert_eq!(TableType::Unlogged.sql_keyword(), "UNLOGGED");
assert_eq!(TableType::Regular.sql_keyword(), "");
}
#[test]
fn test_validation() {
let config = Config::new("");
assert!(config.validate().is_err());
let config = Config::new("postgresql://localhost/test").table_name("");
assert!(config.validate().is_err());
let config = Config::new("postgresql://localhost/test");
assert!(config.validate().is_ok());
}
}