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