razeri-plugin-contract 0.1.1

The dialect contract Razeri plugins are written against — rendering traits, data types, platform identity
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! Storage rendering trait for partition, compression, and index strategies.
//!
//! Each dialect plugin implements [`StorageRenderer`] to translate the agnostic
//! [`StoragePlan`] into native SQL fragments. All methods have default no-op
//! implementations — dialects that lack a feature simply do not override.
//!
//! Two rules bind every implementation: callers must never branch on dialect
//! name, and implementations must never panic on unsupported features — emit a
//! storage warning event and produce no SQL instead.

use crate::data_type::DataType;
use crate::storage_strategy::StoragePlan;

/// Lightweight reference to a target entity for storage rendering.
///
/// Carries only what storage rendering needs, so the contract does not depend
/// on Razeri's full design model.
#[derive(Debug, Clone, Copy)]
pub struct EntityRef<'a> {
    /// Optional schema/database namespace.
    pub schema: Option<&'a str>,
    /// Entity name (without schema qualifier).
    pub name: &'a str,
    /// Whether this rendering targets the entity's identity table (`*_identity`)
    /// rather than the primary table. Identity tables generally do not partition.
    pub is_identity_table: bool,
    /// Column names of the target table, in declaration order. Required by
    /// dialects whose index DDL must enumerate columns explicitly — for
    /// example T-SQL `CREATE NONCLUSTERED COLUMNSTORE INDEX ... (col, ...)`,
    /// which fails with `Msg 35336` if the column list is missing. Empty when
    /// the caller does not have column information; renderers that need
    /// columns must skip emission and warn rather than emit invalid DDL.
    pub columns: &'a [&'a str],
    /// The declared type of the plan's partition column, when the caller knows
    /// it.
    ///
    /// **Some platforms spell a partition differently per column type, and
    /// spell it wrong without this.** `GoogleSQL` accepts
    /// `DATE(<timestamp_column>)` and `DATE(<datetime_column>)`, and it accepts
    /// a `DATE` column only *bare* — `PARTITION BY DATE(<date_column>)` is
    /// refused with the grammar quoted back (measured on the service,
    /// 2026-09-07). A renderer that cannot tell the three apart has to guess,
    /// and one of its guesses is invalid DDL.
    ///
    /// `None` when the caller has no type information; a renderer must then
    /// emit only what is valid for every type it could be, or emit nothing.
    pub partition_column_type: Option<&'a DataType>,
}

/// Renders agnostic [`StoragePlan`] data into platform-native SQL fragments.
///
/// All methods have default implementations that produce no SQL. Dialects
/// override only the methods their platform supports.
pub trait StorageRenderer {
    /// Render any standalone partition function DDL (e.g. T-SQL
    /// `CREATE PARTITION FUNCTION`). Returns a list of statements emitted
    /// before the `CREATE TABLE`.
    #[must_use]
    fn render_partition_function(
        &self,
        _entity: EntityRef<'_>,
        _plan: &StoragePlan,
    ) -> Vec<String> {
        Vec::new()
    }

    /// Render any standalone partition scheme DDL (e.g. T-SQL
    /// `CREATE PARTITION SCHEME`). Returns statements emitted after the
    /// partition function and before the `CREATE TABLE`.
    #[must_use]
    fn render_partition_scheme(&self, _entity: EntityRef<'_>, _plan: &StoragePlan) -> Vec<String> {
        Vec::new()
    }

    /// Render the inline storage clause appended directly to the
    /// `CREATE TABLE` statement (e.g. `PostgreSQL` `PARTITION BY RANGE (...)`,
    /// `ClickHouse` `PARTITION BY toYYYYMM(...) SETTINGS ...`).
    ///
    /// **On a platform whose
    /// [`requires_table_engine`](crate::SqlRenderer::requires_table_engine) is
    /// `true` this clause must contain NEITHER an engine clause nor a sorting
    /// key.** The `CREATE TABLE` composer emits both, and it
    /// is the only thing that can: a sorting key has to begin with the inline
    /// `PRIMARY KEY`, which this function is never handed. A clause that spells
    /// its own produces two engine clauses in one statement — `Code: 62` on
    /// `ClickHouse` — or a key the engine rejects at `Code: 36`. Clustering
    /// intent reaches the sorting key on the table instead, as its clustering
    /// columns.
    #[must_use]
    fn render_table_storage_clause(
        &self,
        _entity: EntityRef<'_>,
        _plan: &StoragePlan,
    ) -> Option<String> {
        None
    }

    /// Render statements emitted after the `CREATE TABLE` (e.g. `PostgreSQL`
    /// child partition `CREATE TABLE ... PARTITION OF ...`, secondary indexes,
    /// `CLUSTER BY`).
    #[must_use]
    fn render_post_create_storage(
        &self,
        _entity: EntityRef<'_>,
        _plan: &StoragePlan,
    ) -> Vec<String> {
        Vec::new()
    }

    /// Render the maintenance procedure that extends the partitioning window
    /// (creates the next partition).
    #[must_use]
    fn render_partition_extend_procedure(
        &self,
        _entity: EntityRef<'_>,
        _plan: &StoragePlan,
    ) -> Option<String> {
        None
    }

    /// Render the maintenance procedure that retains the partitioning window
    /// (drops or archives the oldest partition per the retention policy).
    #[must_use]
    fn render_partition_retain_procedure(
        &self,
        _entity: EntityRef<'_>,
        _plan: &StoragePlan,
    ) -> Option<String> {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct DefaultRenderer;
    impl StorageRenderer for DefaultRenderer {}

    fn entity() -> EntityRef<'static> {
        EntityRef {
            schema: Some("core"),
            name: "events",
            is_identity_table: false,
            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).is_none());
    }
}