|
| 1 | +//! Builder for ALTER TABLE (schema evolution) transactions. |
| 2 | +//! |
| 3 | +//! This module contains [`AlterTableTransactionBuilder`], which uses a type-state pattern to |
| 4 | +//! enforce valid operation chaining at compile time. |
| 5 | +//! |
| 6 | +//! # Type States |
| 7 | +//! |
| 8 | +//! - [`Ready`]: Initial state. Operations are available, but `build()` is not (at least one |
| 9 | +//! operation is required). |
| 10 | +//! - [`Modifying`]: After any chainable schema operation. More ops can be chained, and `build()` is |
| 11 | +//! available. See [`AlterTableTransactionBuilder<Modifying>`] for ops. |
| 12 | +//! |
| 13 | +//! # Transitions |
| 14 | +//! |
| 15 | +//! Each `impl` block below is gated by a state bound and documents which operations that |
| 16 | +//! state enables. Chainable schema operations live on `impl<S: Chainable>` and transition |
| 17 | +//! the builder to a chainable state; `build()` lives on states that are buildable. |
| 18 | +//! |
| 19 | +//! ```ignore |
| 20 | +//! // Allowed: at least one op queued before build(). |
| 21 | +//! snapshot.alter_table().add_column(field).build(engine, committer)?; |
| 22 | +//! |
| 23 | +//! // Not allowed: build() is not defined on Ready (no ops queued). |
| 24 | +//! snapshot.alter_table().build(engine, committer)?; // compile error |
| 25 | +//! ``` |
| 26 | +
|
| 27 | +use std::marker::PhantomData; |
| 28 | +use std::sync::Arc; |
| 29 | + |
| 30 | +use crate::committer::Committer; |
| 31 | +use crate::schema::StructField; |
| 32 | +use crate::snapshot::SnapshotRef; |
| 33 | +use crate::table_configuration::TableConfiguration; |
| 34 | +use crate::table_features::Operation; |
| 35 | +use crate::transaction::alter_table::AlterTableTransaction; |
| 36 | +use crate::transaction::schema_evolution::{ |
| 37 | + apply_schema_operations, SchemaEvolutionResult, SchemaOperation, |
| 38 | +}; |
| 39 | +use crate::{DeltaResult, Engine}; |
| 40 | + |
| 41 | +/// Initial state: `build()` is not yet available (at least one operation is required). |
| 42 | +/// See [`Chainable`] for the operations available on this state. |
| 43 | +pub struct Ready; |
| 44 | + |
| 45 | +/// State after at least one operation has been added. `build()` is available. |
| 46 | +/// See [`Chainable`] for the operations available on this state. |
| 47 | +pub struct Modifying; |
| 48 | + |
| 49 | +/// Marker trait for builder states that accept chainable schema operations. Grouping states |
| 50 | +/// under one bound lets each op (like `add_column`) live on a single `impl<S: Chainable>` |
| 51 | +/// block -- chainable states share the body rather than duplicating it per state. |
| 52 | +/// |
| 53 | +/// Sealed: external types cannot implement this, keeping the set of chainable states closed. |
| 54 | +pub trait Chainable: sealed::Sealed {} |
| 55 | +impl Chainable for Ready {} |
| 56 | +impl Chainable for Modifying {} |
| 57 | + |
| 58 | +mod sealed { |
| 59 | + pub trait Sealed {} |
| 60 | + impl Sealed for super::Ready {} |
| 61 | + impl Sealed for super::Modifying {} |
| 62 | +} |
| 63 | + |
| 64 | +/// Builder for constructing an [`AlterTableTransaction`] with schema evolution operations. |
| 65 | +/// |
| 66 | +/// Uses a type-state pattern (`S`) to enforce at compile time: |
| 67 | +/// - At least one schema operation must be queued before `build()` is callable. |
| 68 | +/// - Only operations valid for the current state can be chained. This will disallow incompatibel |
| 69 | +/// chaining. |
| 70 | +pub struct AlterTableTransactionBuilder<S = Ready> { |
| 71 | + snapshot: SnapshotRef, |
| 72 | + operations: Vec<SchemaOperation>, |
| 73 | + // PhantomData marker for builder state (Ready or Modifying). |
| 74 | + // Zero-sized; only affects which methods are available at compile time. |
| 75 | + _state: PhantomData<S>, |
| 76 | +} |
| 77 | + |
| 78 | +impl<S> AlterTableTransactionBuilder<S> { |
| 79 | + // Reconstructs the builder with a different PhantomData marker, changing which methods |
| 80 | + // are available at compile time (e.g. Ready -> Modifying enables `build()`). All real |
| 81 | + // fields are moved as-is; only the zero-sized type state changes. |
| 82 | + // |
| 83 | + // `T` (distinct from the struct's `S`) lets the caller pick the target state: |
| 84 | + // `self.transition::<Modifying>()` returns `AlterTableTransactionBuilder<Modifying>`. |
| 85 | + fn transition<T>(self) -> AlterTableTransactionBuilder<T> { |
| 86 | + AlterTableTransactionBuilder { |
| 87 | + snapshot: self.snapshot, |
| 88 | + operations: self.operations, |
| 89 | + _state: PhantomData, |
| 90 | + } |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +impl AlterTableTransactionBuilder<Ready> { |
| 95 | + /// Create a new builder from a snapshot. |
| 96 | + pub(crate) fn new(snapshot: SnapshotRef) -> Self { |
| 97 | + AlterTableTransactionBuilder { |
| 98 | + snapshot, |
| 99 | + operations: Vec::new(), |
| 100 | + _state: PhantomData, |
| 101 | + } |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +impl<S: Chainable> AlterTableTransactionBuilder<S> { |
| 106 | + /// Add a new top-level column to the table schema. |
| 107 | + /// |
| 108 | + /// The field must not already exist in the schema (case-insensitive). The field must be |
| 109 | + /// nullable because existing data files do not contain this column and will read NULL for it. |
| 110 | + /// These constraints are validated during [`build()`](AlterTableTransactionBuilder::build). |
| 111 | + pub fn add_column(mut self, field: StructField) -> AlterTableTransactionBuilder<Modifying> { |
| 112 | + self.operations.push(SchemaOperation::AddColumn { field }); |
| 113 | + self.transition() |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +impl AlterTableTransactionBuilder<Modifying> { |
| 118 | + /// Validate and apply schema operations, then build the [`AlterTableTransaction`]. |
| 119 | + /// |
| 120 | + /// This method: |
| 121 | + /// 1. Validates the table supports writes |
| 122 | + /// 2. Applies each operation sequentially against the evolving schema |
| 123 | + /// 3. Constructs new Metadata action with evolved schema |
| 124 | + /// 4. Builds the evolved table configuration |
| 125 | + /// 5. Creates the transaction |
| 126 | + /// |
| 127 | + /// # Errors |
| 128 | + /// |
| 129 | + /// - Any individual operation fails validation (see per-method errors above) |
| 130 | + /// - Table does not support writes (unsupported features) |
| 131 | + /// - The evolved schema requires protocol features not enabled on the table (e.g. adding a |
| 132 | + /// `timestampNtz` column without the `timestampNtz` feature) |
| 133 | + pub fn build( |
| 134 | + self, |
| 135 | + _engine: &dyn Engine, |
| 136 | + committer: Box<dyn Committer>, |
| 137 | + ) -> DeltaResult<AlterTableTransaction> { |
| 138 | + let table_config = self.snapshot.table_configuration(); |
| 139 | + // Rejects writes to tables kernel can't safely commit to: writer version out of |
| 140 | + // kernel's supported range, unsupported writer features, or schemas with SQL-expression |
| 141 | + // invariants. Runs on the pre-alter snapshot; future ALTER variants that change the |
| 142 | + // protocol must also re-check this on the evolved `TableConfiguration`. |
| 143 | + table_config.ensure_operation_supported(Operation::Write)?; |
| 144 | + |
| 145 | + let schema = Arc::unwrap_or_clone(table_config.logical_schema()); |
| 146 | + let SchemaEvolutionResult { |
| 147 | + schema: evolved_schema, |
| 148 | + } = apply_schema_operations(schema, self.operations, table_config.column_mapping_mode())?; |
| 149 | + |
| 150 | + let evolved_metadata = table_config |
| 151 | + .metadata() |
| 152 | + .clone() |
| 153 | + .with_schema(evolved_schema.clone())?; |
| 154 | + |
| 155 | + // Validates the evolved metadata against the protocol. |
| 156 | + let evolved_table_config = TableConfiguration::try_new_with_schema( |
| 157 | + table_config, |
| 158 | + evolved_metadata, |
| 159 | + evolved_schema, |
| 160 | + )?; |
| 161 | + |
| 162 | + AlterTableTransaction::try_new_alter_table(self.snapshot, evolved_table_config, committer) |
| 163 | + } |
| 164 | +} |
0 commit comments