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}