use crate::data_type::DataType;
use crate::storage_strategy::StoragePlan;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TableRole {
#[default]
Written,
Landing,
Identity,
}
#[derive(Debug, Clone, Copy)]
pub struct EntityRef<'a> {
pub schema: Option<&'a str>,
pub name: &'a str,
pub role: TableRole,
pub columns: &'a [&'a str],
pub partition_column_type: Option<&'a DataType>,
}
#[derive(Debug, Clone, Copy)]
pub struct SliceRef<'a> {
pub column: &'a str,
pub value: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SliceReplacement {
Emptied(Vec<String>),
WrittenByInsert(String),
}
#[derive(Debug, Clone, Copy)]
pub struct RetainedCompanion<'a> {
pub table: EntityRef<'a>,
pub slice_column: &'a str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtendCall {
pub procedure: String,
pub after_the_day: Vec<String>,
pub runs: ExtendRuns,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtendRuns {
InsideTheLoad,
BeforeTheLoad,
}
pub trait StorageRenderer {
#[must_use]
fn render_partition_function(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
) -> Vec<String> {
Vec::new()
}
#[must_use]
fn render_partition_scheme(&self, _entity: EntityRef<'_>, _plan: &StoragePlan) -> Vec<String> {
Vec::new()
}
#[must_use]
fn render_table_storage_clause(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
) -> Option<String> {
None
}
#[must_use]
fn render_post_create_storage(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
) -> Vec<String> {
Vec::new()
}
#[must_use]
fn render_partition_extend_procedure(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
) -> Option<String> {
None
}
#[must_use]
fn render_partition_retain_procedure(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
_also: Option<RetainedCompanion<'_>>,
) -> Option<String> {
None
}
#[must_use]
fn partition_extend_call(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
) -> Option<ExtendCall> {
None
}
#[must_use]
fn render_slice_replacement(
&self,
_entity: EntityRef<'_>,
_plan: &StoragePlan,
_slice: SliceRef<'_>,
) -> Option<SliceReplacement> {
None
}
#[must_use]
fn slice_replacement_is_procedure_body(&self, replacement: &SliceReplacement) -> bool {
let SliceReplacement::Emptied(statements) = replacement else {
return false;
};
statements.iter().any(|statement| {
let opens_a_conditional = statement
.split_whitespace()
.next()
.is_some_and(|word| word.eq_ignore_ascii_case("IF"));
opens_a_conditional && statement.to_ascii_uppercase().contains("END IF")
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct DefaultRenderer;
impl StorageRenderer for DefaultRenderer {}
fn entity() -> EntityRef<'static> {
EntityRef {
schema: Some("core"),
name: "events",
role: TableRole::Written,
columns: &[],
partition_column_type: None,
}
}
#[test]
fn defaults_produce_no_sql() {
let r = DefaultRenderer;
let plan = StoragePlan::none();
assert!(r.render_partition_function(entity(), &plan).is_empty());
assert!(r.render_partition_scheme(entity(), &plan).is_empty());
assert!(r.render_table_storage_clause(entity(), &plan).is_none());
assert!(r.render_post_create_storage(entity(), &plan).is_empty());
assert!(r.render_partition_extend_procedure(entity(), &plan).is_none());
assert!(r.render_partition_retain_procedure(entity(), &plan, None).is_none());
assert!(r.partition_extend_call(entity(), &plan).is_none());
assert!(
r.render_slice_replacement(
entity(),
&plan,
SliceRef { column: "day", value: "$p_slice" }
)
.is_none()
);
}
#[test]
fn the_default_reads_the_shape_of_the_answer() {
let r = DefaultRenderer;
assert!(r.slice_replacement_is_procedure_body(&SliceReplacement::Emptied(vec![
"IF to_regclass('core.events_20260917') IS NOT NULL THEN\n TRUNCATE TABLE \
core.events_20260917;\nEND IF;"
.to_owned()
])));
assert!(!r.slice_replacement_is_procedure_body(&SliceReplacement::Emptied(vec![
"ALTER TABLE core.events DROP PARTITION 20260917;".to_owned()
])));
assert!(!r.slice_replacement_is_procedure_body(&SliceReplacement::WrittenByInsert(
"REPLACE WHERE day = '2026-09-17'".to_owned()
)));
}
#[test]
fn an_engine_that_states_it_runs_alone_is_believed() {
struct RunsAlone;
impl StorageRenderer for RunsAlone {
fn slice_replacement_is_procedure_body(&self, _replacement: &SliceReplacement) -> bool {
false
}
}
let conditional = SliceReplacement::Emptied(vec![
"IF EXISTS (SELECT 1 FROM core.events WHERE day = '2026-09-17') THEN\n DELETE FROM \
core.events WHERE day = '2026-09-17';\nEND IF;"
.to_owned(),
]);
assert!(DefaultRenderer.slice_replacement_is_procedure_body(&conditional));
assert!(!RunsAlone.slice_replacement_is_procedure_body(&conditional));
}
}