Skip to main content

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}