use futures::{StreamExt, stream};
use sea_query::{Condition, ExprTrait, Query};
use super::DryRun;
use super::utils::LookupTableHandle;
use crate::spec::*;
use crate::{Ducklake, DucklakeResult, io};
const DATA_FILE_ID_LOOKUP_TABLE: &str = "__ducklake_cleaned_up_data_file_ids";
impl Ducklake {
pub async fn cleanup_old_files(&self, dry_run: DryRun) -> DucklakeResult<Vec<String>> {
let interval = self.conn.metadata().delete_older_than();
let timestamp = chrono::Utc::now() - interval.months - interval.delta;
self.cleanup_old_files_filtered(CleanupFilter::OlderThan(timestamp), dry_run)
.await
}
pub async fn cleanup_old_files_older_than(
&self,
timestamp: chrono::DateTime<chrono::Utc>,
dry_run: DryRun,
) -> DucklakeResult<Vec<String>> {
self.cleanup_old_files_filtered(CleanupFilter::OlderThan(timestamp), dry_run)
.await
}
pub async fn cleanup_all_old_files(&self, dry_run: DryRun) -> DucklakeResult<Vec<String>> {
self.cleanup_old_files_filtered(CleanupFilter::All, dry_run)
.await
}
async fn cleanup_old_files_filtered(
&self,
filter: CleanupFilter,
dry_run: DryRun,
) -> DucklakeResult<Vec<String>> {
let data_path = self.conn.metadata().data_path();
let storage_options = self.conn.storage_options().to_vec();
let mut select_query = Query::select()
.columns([
ducklake_files_scheduled_for_deletion::Column::DataFileId,
ducklake_files_scheduled_for_deletion::Column::Path,
ducklake_files_scheduled_for_deletion::Column::PathIsRelative,
])
.from(ducklake_files_scheduled_for_deletion::Table)
.take();
if let Some(condition) = filter.condition() {
select_query.cond_where(condition);
}
let files: Vec<(i64, String, bool)> = self.conn.pool().fetch_all(&select_query).await?;
let paths = files
.iter()
.map(|(_, path, path_is_relative)| {
let stored = io::DucklakePath::new(path, *path_is_relative);
data_path.join(&stored)
})
.collect::<Vec<_>>();
if matches!(dry_run, DryRun::Yes) {
return Ok(paths.iter().map(|path| path.to_string()).collect());
}
self.conn.check_writable()?;
delete_files(&paths, &storage_options).await?;
let mut tx = self.conn.pool().begin().await?;
let file_ids = files.into_iter().map(|(id, _, _)| id).collect::<Vec<_>>();
let lookup_table =
LookupTableHandle::new(&mut tx, DATA_FILE_ID_LOOKUP_TABLE, &file_ids).await?;
let delete_query =
Query::delete()
.from_table(ducklake_files_scheduled_for_deletion::Table)
.cond_where(lookup_table.condition_is_in(
ducklake_files_scheduled_for_deletion::Column::DataFileId.col(),
))
.take();
tx.execute(&delete_query).await?;
lookup_table.drop(&mut tx).await?;
tx.commit().await?;
Ok(paths.into_iter().map(|path| path.to_string()).collect())
}
}
async fn delete_files(
paths: &[io::DucklakePath],
storage_options: &[(String, String)],
) -> DucklakeResult<()> {
let Some(first) = paths.first() else {
return Ok(());
};
let store = first
.resolve()?
.object_store(Some(storage_options.to_vec()));
let locations = paths
.iter()
.map(|path| Ok(path.resolve()?.path()))
.collect::<DucklakeResult<Vec<_>>>()?;
let mut stream = store.delete_stream(stream::iter(locations.into_iter().map(Ok)).boxed());
while let Some(result) = stream.next().await {
match result {
Ok(_) | Err(object_store::Error::NotFound { .. }) => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
enum CleanupFilter {
All,
OlderThan(chrono::DateTime<chrono::Utc>),
}
impl CleanupFilter {
fn condition(&self) -> Option<Condition> {
match self {
Self::All => None,
Self::OlderThan(timestamp) => Some(
ducklake_files_scheduled_for_deletion::Column::ScheduleStart
.col()
.lt(*timestamp)
.into(),
),
}
}
}