use crate::error::Result;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartitionConfig {
pub enabled: bool,
pub strategy: PartitionStrategy,
pub retention_months: u32,
pub precreate_months: u32,
}
impl Default for PartitionConfig {
fn default() -> Self {
Self {
enabled: true,
strategy: PartitionStrategy::Monthly,
retention_months: 12,
precreate_months: 3,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartitionInfo {
pub name: String,
pub table_name: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub created: bool,
}
impl PartitionInfo {
pub fn new(date: DateTime<Utc>, table_name: &str) -> Result<Self> {
use crate::error::CacheError;
use chrono::{Datelike, TimeZone};
let year = date.year();
let month = date.month();
let start_date = Utc
.with_ymd_and_hms(year, month, 1, 0, 0, 0)
.single()
.ok_or_else(|| {
CacheError::DatabaseError(format!("Invalid start date for {}-{}", year, month))
})?;
let (next_year, next_month) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
let end_date = Utc
.with_ymd_and_hms(next_year, next_month, 1, 0, 0, 0)
.single()
.ok_or_else(|| {
CacheError::DatabaseError(format!(
"Invalid end date for {}-{}",
next_year, next_month
))
})?;
let name = format!("{}_{:04}_{:02}", table_name, year, month);
Ok(Self {
name,
table_name: table_name.to_string(),
start_date,
end_date,
created: false,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PartitionStrategy {
Monthly,
Range,
}
#[async_trait]
pub trait PartitionManager: Send + Sync {
async fn initialize_table(&self, table_name: &str, schema: &str) -> Result<()>;
async fn create_partition(&self, partition: &PartitionInfo) -> Result<()>;
async fn get_partitions(&self, table_name: &str) -> Result<Vec<PartitionInfo>>;
async fn drop_partition(&self, table_name: &str, partition_name: &str) -> Result<()>;
async fn ensure_partition_exists(
&self,
date: DateTime<Utc>,
table_name: &str,
) -> Result<String>;
async fn precreate_partitions(&self, table_name: &str, months_ahead: u32) -> Result<()> {
use chrono::{Datelike, TimeZone};
let now = Utc::now();
for i in 0..=months_ahead {
let mut target_month = now.month() + i;
let mut target_year = now.year();
while target_month > 12 {
target_month -= 12;
target_year += 1;
}
if let Some(target_date) = Utc
.with_ymd_and_hms(target_year, target_month, 1, 0, 0, 0)
.single()
{
self.ensure_partition_exists(target_date, table_name)
.await?;
} else {
tracing::warn!(
"Failed to construct date for partition: {}-{}",
target_year,
target_month
);
}
}
Ok(())
}
async fn cleanup_old_partitions(
&self,
table_name: &str,
cutoff_date: DateTime<Utc>,
) -> Result<u32> {
let partitions = self.get_partitions(table_name).await?;
let mut dropped_count = 0;
for partition in partitions {
if partition.end_date < cutoff_date {
self.drop_partition(table_name, &partition.name).await?;
dropped_count += 1;
}
}
Ok(dropped_count)
}
fn get_config(&self) -> PartitionConfig;
fn extract_base_table(&self, table_name: &str) -> String {
let parts: Vec<&str> = table_name.split('_').collect();
if parts.len() >= 3 {
let year = parts[parts.len() - 2].parse::<i32>();
let month = parts[parts.len() - 1].parse::<u32>();
if year.is_ok() && month.is_ok() {
return parts[..parts.len() - 2].join("_");
}
}
table_name.to_string()
}
fn generate_partition_name(&self, date: &DateTime<Utc>, prefix: &str) -> String {
use chrono::Datelike;
format!("{}_{:04}_{:02}", prefix, date.year(), date.month())
}
fn generate_partition_table_name(&self, table_name: &str, date: &DateTime<Utc>) -> String {
self.generate_partition_name(date, table_name)
}
fn parse_partition_date(&self, table_name: &str) -> Option<DateTime<Utc>> {
use chrono::{TimeZone, Utc};
let parts: Vec<&str> = table_name.split('_').collect();
if parts.len() >= 3 {
let year_str = parts[parts.len() - 2];
let month_str = parts[parts.len() - 1];
if let (Ok(year), Ok(month)) = (year_str.parse::<i32>(), month_str.parse::<u32>()) {
return Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0).single();
}
}
None
}
fn get_next_month_first_day(&self, date: &DateTime<Utc>) -> DateTime<Utc> {
use chrono::{Datelike, TimeZone, Utc};
let (year, month) = if date.month() == 12 {
(date.year() + 1, 1)
} else {
(date.year(), date.month() + 1)
};
Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0)
.single()
.unwrap_or_else(Utc::now) }
}