@@ -44,6 +44,31 @@ pub trait SchemaOps {
4444pub struct StrictSchema {
4545 pub columns : Vec < ColumnDef > ,
4646 pub version : u16 ,
47+ /// Columns that were removed via `ALTER DROP COLUMN`. Retained so the
48+ /// reader can reconstruct the physical layout of tuples written before
49+ /// the drop.
50+ #[ serde( default , skip_serializing_if = "Vec::is_empty" ) ]
51+ pub dropped_columns : Vec < DroppedColumn > ,
52+ }
53+
54+ /// Tombstone for a column removed by `ALTER DROP COLUMN`.
55+ #[ derive(
56+ Debug ,
57+ Clone ,
58+ PartialEq ,
59+ Eq ,
60+ Serialize ,
61+ Deserialize ,
62+ zerompk:: ToMessagePack ,
63+ zerompk:: FromMessagePack ,
64+ ) ]
65+ pub struct DroppedColumn {
66+ /// The full column definition at time of drop.
67+ pub def : ColumnDef ,
68+ /// The column's position in the column list before it was removed.
69+ pub position : usize ,
70+ /// The schema version at which the column was dropped.
71+ pub dropped_at_version : u16 ,
4772}
4873
4974/// Schema for a columnar collection (compressed segment files).
@@ -112,6 +137,7 @@ impl StrictSchema {
112137 Ok ( Self {
113138 columns,
114139 version : 1 ,
140+ dropped_columns : Vec :: new ( ) ,
115141 } )
116142 }
117143
@@ -135,6 +161,75 @@ impl StrictSchema {
135161 pub fn null_bitmap_size ( & self ) -> usize {
136162 self . columns . len ( ) . div_ceil ( 8 )
137163 }
164+
165+ /// Build a sub-schema matching the physical layout of tuples written at
166+ /// the given version. Columns added after `version` are excluded;
167+ /// columns dropped after `version` are re-inserted at their original
168+ /// positions.
169+ pub fn schema_for_version ( & self , version : u16 ) -> StrictSchema {
170+ // Start with live columns that existed at this version.
171+ let mut cols: Vec < ColumnDef > = self
172+ . columns
173+ . iter ( )
174+ . filter ( |c| c. added_at_version <= version)
175+ . cloned ( )
176+ . collect ( ) ;
177+
178+ // Re-insert dropped columns that were still alive at this version,
179+ // sorted by position (ascending) so inserts don't shift later indices.
180+ let mut to_reinsert: Vec < & DroppedColumn > = self
181+ . dropped_columns
182+ . iter ( )
183+ . filter ( |dc| dc. def . added_at_version <= version && dc. dropped_at_version > version)
184+ . collect ( ) ;
185+ to_reinsert. sort_by_key ( |dc| dc. position ) ;
186+ for dc in to_reinsert {
187+ let pos = dc. position . min ( cols. len ( ) ) ;
188+ cols. insert ( pos, dc. def . clone ( ) ) ;
189+ }
190+
191+ StrictSchema {
192+ version,
193+ columns : cols,
194+ dropped_columns : Vec :: new ( ) ,
195+ }
196+ }
197+
198+ /// Parse a SQL default literal (e.g. `'n/a'`, `0`, `true`) into a `Value`.
199+ ///
200+ /// Covers the common cases produced by `ALTER ADD COLUMN ... DEFAULT ...`.
201+ /// Returns `Value::Null` for expressions that cannot be trivially evaluated
202+ /// at read time (functions, sub-queries, etc.).
203+ pub fn parse_default_literal ( expr : & str ) -> crate :: value:: Value {
204+ use crate :: value:: Value ;
205+
206+ let trimmed = expr. trim ( ) ;
207+
208+ // String literals: 'foo'
209+ if trimmed. starts_with ( '\'' ) && trimmed. ends_with ( '\'' ) && trimmed. len ( ) >= 2 {
210+ return Value :: String ( trimmed[ 1 ..trimmed. len ( ) - 1 ] . replace ( "''" , "'" ) ) ;
211+ }
212+
213+ // Boolean
214+ match trimmed. to_uppercase ( ) . as_str ( ) {
215+ "TRUE" => return Value :: Bool ( true ) ,
216+ "FALSE" => return Value :: Bool ( false ) ,
217+ "NULL" => return Value :: Null ,
218+ _ => { }
219+ }
220+
221+ // Integer
222+ if let Ok ( i) = trimmed. parse :: < i64 > ( ) {
223+ return Value :: Integer ( i) ;
224+ }
225+
226+ // Float
227+ if let Ok ( f) = trimmed. parse :: < f64 > ( ) {
228+ return Value :: Float ( f) ;
229+ }
230+
231+ Value :: Null
232+ }
138233}
139234
140235impl ColumnarSchema {
@@ -211,6 +306,7 @@ mod tests {
211306 modifiers: Vec :: new( ) ,
212307 generated_expr: None ,
213308 generated_deps: Vec :: new( ) ,
309+ added_at_version: 1 ,
214310 } ] ;
215311 assert ! ( matches!(
216312 StrictSchema :: new( cols) ,
0 commit comments