Skip to main content

razeri_plugin_contract/
storage_renderer.rs

1// Copyright (C) 2026 Gabriel Shea <gshea@ideapivot.com> - All Rights Reserved
2
3//! Storage rendering trait for partition, compression, and index strategies.
4//!
5//! Each dialect plugin implements [`StorageRenderer`] to translate the agnostic
6//! [`StoragePlan`] into native SQL fragments. All methods have default no-op
7//! implementations — dialects that lack a feature simply do not override.
8//!
9//! Per ADR `TP-004` callers must never branch on dialect name; per ADR `TP-010`
10//! implementations must never panic on unsupported features (emit a
11//! `RazeriEvent::StorageWarning` instead).
12
13use crate::data_type::DataType;
14use crate::storage_strategy::StoragePlan;
15
16/// Lightweight reference to a target entity for storage rendering.
17///
18/// Avoids pulling the full design `Entity` into `razeri-render-api`.
19#[derive(Debug, Clone, Copy)]
20pub struct EntityRef<'a> {
21    /// Optional schema/database namespace.
22    pub schema: Option<&'a str>,
23    /// Entity name (without schema qualifier).
24    pub name: &'a str,
25    /// Whether this rendering targets the entity's identity table (`*_identity`)
26    /// rather than the primary table. Identity tables generally do not partition.
27    pub is_identity_table: bool,
28    /// Column names of the target table, in declaration order. Required by
29    /// dialects whose index DDL must enumerate columns explicitly — for
30    /// example T-SQL `CREATE NONCLUSTERED COLUMNSTORE INDEX ... (col, ...)`,
31    /// which fails with `Msg 35336` if the column list is missing. Empty when
32    /// the caller does not have column information; renderers that need
33    /// columns must skip emission and warn rather than emit invalid DDL.
34    pub columns: &'a [&'a str],
35    /// The declared type of the plan's partition column, when the caller knows
36    /// it.
37    ///
38    /// **Some platforms spell a partition differently per column type, and
39    /// spell it wrong without this.** `GoogleSQL` accepts
40    /// `DATE(<timestamp_column>)` and `DATE(<datetime_column>)`, and it accepts
41    /// a `DATE` column only *bare* — `PARTITION BY DATE(<date_column>)` is
42    /// refused with the grammar quoted back (measured on the service,
43    /// 2026-09-07). A renderer that cannot tell the three apart has to guess,
44    /// and one of its guesses is invalid DDL.
45    ///
46    /// `None` when the caller has no type information; a renderer must then
47    /// emit only what is valid for every type it could be, or emit nothing.
48    pub partition_column_type: Option<&'a DataType>,
49}
50
51/// Renders agnostic [`StoragePlan`] data into platform-native SQL fragments.
52///
53/// All methods have default implementations that produce no SQL. Dialects
54/// override only the methods their platform supports.
55pub trait StorageRenderer {
56    /// Render any standalone partition function DDL (e.g. T-SQL
57    /// `CREATE PARTITION FUNCTION`). Returns a list of statements emitted
58    /// before the `CREATE TABLE`.
59    #[must_use]
60    fn render_partition_function(
61        &self,
62        _entity: EntityRef<'_>,
63        _plan: &StoragePlan,
64    ) -> Vec<String> {
65        Vec::new()
66    }
67
68    /// Render any standalone partition scheme DDL (e.g. T-SQL
69    /// `CREATE PARTITION SCHEME`). Returns statements emitted after the
70    /// partition function and before the `CREATE TABLE`.
71    #[must_use]
72    fn render_partition_scheme(&self, _entity: EntityRef<'_>, _plan: &StoragePlan) -> Vec<String> {
73        Vec::new()
74    }
75
76    /// Render the inline storage clause appended directly to the
77    /// `CREATE TABLE` statement (e.g. `PostgreSQL` `PARTITION BY RANGE (...)`,
78    /// `ClickHouse` `PARTITION BY toYYYYMM(...) SETTINGS ...`).
79    ///
80    /// **On a platform whose
81    /// [`requires_table_engine`](crate::SqlRenderer::requires_table_engine) is
82    /// `true` this clause must contain NEITHER an engine clause nor a sorting
83    /// key.** The `CREATE TABLE` composer emits both, and it
84    /// is the only thing that can: a sorting key has to begin with the inline
85    /// `PRIMARY KEY`, which this function is never handed. A clause that spells
86    /// its own produces two engine clauses in one statement — `Code: 62` on
87    /// `ClickHouse` — or a key the engine rejects at `Code: 36`. Clustering
88    /// intent reaches the sorting key on the table instead, as its clustering
89    /// columns.
90    #[must_use]
91    fn render_table_storage_clause(
92        &self,
93        _entity: EntityRef<'_>,
94        _plan: &StoragePlan,
95    ) -> Option<String> {
96        None
97    }
98
99    /// Render statements emitted after the `CREATE TABLE` (e.g. `PostgreSQL`
100    /// child partition `CREATE TABLE ... PARTITION OF ...`, secondary indexes,
101    /// `CLUSTER BY`).
102    #[must_use]
103    fn render_post_create_storage(
104        &self,
105        _entity: EntityRef<'_>,
106        _plan: &StoragePlan,
107    ) -> Vec<String> {
108        Vec::new()
109    }
110
111    /// Render the maintenance procedure that extends the partitioning window
112    /// (creates the next partition).
113    #[must_use]
114    fn render_partition_extend_procedure(
115        &self,
116        _entity: EntityRef<'_>,
117        _plan: &StoragePlan,
118    ) -> Option<String> {
119        None
120    }
121
122    /// Render the maintenance procedure that retains the partitioning window
123    /// (drops or archives the oldest partition per the retention policy).
124    #[must_use]
125    fn render_partition_retain_procedure(
126        &self,
127        _entity: EntityRef<'_>,
128        _plan: &StoragePlan,
129    ) -> Option<String> {
130        None
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    struct DefaultRenderer;
139    impl StorageRenderer for DefaultRenderer {}
140
141    fn entity() -> EntityRef<'static> {
142        EntityRef {
143            schema: Some("core"),
144            name: "events",
145            is_identity_table: false,
146            columns: &[],
147            partition_column_type: None,
148        }
149    }
150
151    #[test]
152    fn defaults_produce_no_sql() {
153        let r = DefaultRenderer;
154        let plan = StoragePlan::none();
155        assert!(r.render_partition_function(entity(), &plan).is_empty());
156        assert!(r.render_partition_scheme(entity(), &plan).is_empty());
157        assert!(r.render_table_storage_clause(entity(), &plan).is_none());
158        assert!(r.render_post_create_storage(entity(), &plan).is_empty());
159        assert!(r.render_partition_extend_procedure(entity(), &plan).is_none());
160        assert!(r.render_partition_retain_procedure(entity(), &plan).is_none());
161    }
162}