Skip to main content

cobble_table/
logical_type.rs

1use crate::{Result, TableError};
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4
5/// Stable identity of a field within a table schema.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
7#[serde(transparent)]
8pub struct FieldId(pub u32);
9
10impl From<u32> for FieldId {
11    fn from(value: u32) -> Self {
12        Self(value)
13    }
14}
15
16/// Timestamp semantic kind. Both kinds share the same physical encoding.
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum TimestampKind {
20    WithoutTimeZone,
21    WithLocalTimeZone,
22}
23
24/// Provider-defined semantics with a portable physical fallback.
25#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct ExtensionType {
27    pub type_id: String,
28    pub parameters: JsonValue,
29    pub physical_type: Box<LogicalType>,
30}
31
32impl ExtensionType {
33    pub fn new(
34        type_id: impl Into<String>,
35        parameters: JsonValue,
36        physical_type: LogicalType,
37    ) -> Result<Self> {
38        let extension = Self {
39            type_id: type_id.into(),
40            parameters,
41            physical_type: Box::new(physical_type),
42        };
43        extension.validate()?;
44        Ok(extension)
45    }
46
47    pub(crate) fn validate(&self) -> Result<()> {
48        if self.type_id.trim().is_empty() {
49            return Err(TableError::InvalidSchema(
50                "extension type id must not be empty".to_string(),
51            ));
52        }
53        self.physical_type.validate()
54    }
55}
56
57/// Cross-language logical type, including nullability at every nesting level.
58#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct LogicalType {
60    pub nullable: bool,
61    #[serde(flatten)]
62    pub kind: LogicalTypeKind,
63}
64
65/// Shape and parameters of a logical type.
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(tag = "kind", rename_all = "snake_case")]
68pub enum LogicalTypeKind {
69    Boolean,
70    Int8,
71    Int16,
72    Int32,
73    Int64,
74    Float32,
75    Float64,
76    Decimal {
77        precision: u8,
78        scale: u8,
79    },
80    Date,
81    Time {
82        precision: u8,
83    },
84    Timestamp {
85        precision: u8,
86        timestamp_kind: TimestampKind,
87    },
88    String,
89    Binary,
90    List {
91        element_type: Box<LogicalType>,
92    },
93    Map {
94        key_type: Box<LogicalType>,
95        value_type: Box<LogicalType>,
96    },
97    Struct {
98        fields: Vec<DataField>,
99    },
100    Extension {
101        extension: ExtensionType,
102    },
103}
104
105impl LogicalType {
106    fn new(kind: LogicalTypeKind) -> Self {
107        Self {
108            nullable: false,
109            kind,
110        }
111    }
112
113    pub fn boolean() -> Self {
114        Self::new(LogicalTypeKind::Boolean)
115    }
116
117    pub fn int8() -> Self {
118        Self::new(LogicalTypeKind::Int8)
119    }
120
121    pub fn int16() -> Self {
122        Self::new(LogicalTypeKind::Int16)
123    }
124
125    pub fn int32() -> Self {
126        Self::new(LogicalTypeKind::Int32)
127    }
128
129    pub fn int64() -> Self {
130        Self::new(LogicalTypeKind::Int64)
131    }
132
133    pub fn float32() -> Self {
134        Self::new(LogicalTypeKind::Float32)
135    }
136
137    pub fn float64() -> Self {
138        Self::new(LogicalTypeKind::Float64)
139    }
140
141    pub fn decimal(precision: u8, scale: u8) -> Self {
142        Self::new(LogicalTypeKind::Decimal { precision, scale })
143    }
144
145    pub fn date() -> Self {
146        Self::new(LogicalTypeKind::Date)
147    }
148
149    pub fn time(precision: u8) -> Self {
150        Self::new(LogicalTypeKind::Time { precision })
151    }
152
153    pub fn timestamp(precision: u8, timestamp_kind: TimestampKind) -> Self {
154        Self::new(LogicalTypeKind::Timestamp {
155            precision,
156            timestamp_kind,
157        })
158    }
159
160    pub fn string() -> Self {
161        Self::new(LogicalTypeKind::String)
162    }
163
164    pub fn binary() -> Self {
165        Self::new(LogicalTypeKind::Binary)
166    }
167
168    pub fn list(element_type: LogicalType) -> Self {
169        Self::new(LogicalTypeKind::List {
170            element_type: Box::new(element_type),
171        })
172    }
173
174    pub fn map(key_type: LogicalType, value_type: LogicalType) -> Self {
175        Self::new(LogicalTypeKind::Map {
176            key_type: Box::new(key_type),
177            value_type: Box::new(value_type),
178        })
179    }
180
181    pub fn struct_type(fields: Vec<DataField>) -> Self {
182        Self::new(LogicalTypeKind::Struct { fields })
183    }
184
185    /// Build a fresh struct type without requiring callers to assign field ids.
186    ///
187    /// The assigned ids are local to this type and are renumbered again when
188    /// the type is included in a [`crate::TableSchemaBuilder`]. Use
189    /// [`Self::struct_type`] with explicit [`FieldId`] values only when
190    /// restoring or constructing an already identified schema.
191    pub fn struct_from_fields<I, N>(fields: I) -> Result<Self>
192    where
193        I: IntoIterator<Item = (N, LogicalType)>,
194        N: Into<String>,
195    {
196        let mut fields = fields
197            .into_iter()
198            .map(|(name, logical_type)| DataField {
199                id: FieldId(0),
200                name: name.into(),
201                logical_type,
202            })
203            .collect::<Vec<_>>();
204        let mut next_id = 0;
205        assign_fresh_field_ids(&mut fields, &mut next_id)?;
206        let logical_type = Self::struct_type(fields);
207        logical_type.validate()?;
208        Ok(logical_type)
209    }
210
211    pub fn extension(extension: ExtensionType) -> Self {
212        Self::new(LogicalTypeKind::Extension { extension })
213    }
214
215    #[must_use]
216    pub fn nullable(mut self) -> Self {
217        self.nullable = true;
218        self
219    }
220
221    #[must_use]
222    pub fn not_null(mut self) -> Self {
223        self.nullable = false;
224        self
225    }
226
227    pub(crate) fn validate(&self) -> Result<()> {
228        match &self.kind {
229            LogicalTypeKind::Decimal { precision, scale }
230                if *precision == 0 || *precision > 38 || scale > precision =>
231            {
232                return Err(TableError::InvalidSchema(format!(
233                    "invalid decimal precision/scale: {precision}/{scale}"
234                )));
235            }
236            LogicalTypeKind::Time { precision } | LogicalTypeKind::Timestamp { precision, .. }
237                if *precision > 9 =>
238            {
239                return Err(TableError::InvalidSchema(format!(
240                    "time precision must be in [0, 9], got {precision}"
241                )));
242            }
243            LogicalTypeKind::List { element_type } => element_type.validate()?,
244            LogicalTypeKind::Map {
245                key_type,
246                value_type,
247            } => {
248                if key_type.nullable {
249                    return Err(TableError::InvalidSchema(
250                        "map key type must not be nullable".to_string(),
251                    ));
252                }
253                key_type.validate()?;
254                value_type.validate()?;
255            }
256            LogicalTypeKind::Struct { fields } => {
257                for field in fields {
258                    field.validate()?;
259                }
260            }
261            LogicalTypeKind::Extension { extension } => extension.validate()?,
262            _ => {}
263        }
264        Ok(())
265    }
266
267    pub(crate) fn is_key_compatible(&self) -> bool {
268        !self.nullable
269            && matches!(
270                self.kind,
271                LogicalTypeKind::Boolean
272                    | LogicalTypeKind::Int8
273                    | LogicalTypeKind::Int16
274                    | LogicalTypeKind::Int32
275                    | LogicalTypeKind::Int64
276                    | LogicalTypeKind::Decimal { .. }
277                    | LogicalTypeKind::Date
278                    | LogicalTypeKind::Time { .. }
279                    | LogicalTypeKind::Timestamp { .. }
280                    | LogicalTypeKind::String
281                    | LogicalTypeKind::Binary
282            )
283    }
284}
285
286/// Named field in a top-level table row or nested struct.
287#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
288pub struct DataField {
289    pub id: FieldId,
290    pub name: String,
291    pub logical_type: LogicalType,
292}
293
294impl DataField {
295    /// Construct a field with an explicit persisted id.
296    ///
297    /// Prefer [`crate::TableSchemaBuilder`] for fresh schemas. This constructor
298    /// is intended for advanced uses such as restoration and interoperating
299    /// with a schema whose field identities are already known.
300    pub fn new(
301        id: impl Into<FieldId>,
302        name: impl Into<String>,
303        logical_type: LogicalType,
304    ) -> Result<Self> {
305        let field = Self {
306            id: id.into(),
307            name: name.into(),
308            logical_type,
309        };
310        field.validate()?;
311        Ok(field)
312    }
313
314    pub(crate) fn validate(&self) -> Result<()> {
315        if self.name.trim().is_empty() {
316            return Err(TableError::InvalidSchema(format!(
317                "field {} name must not be empty",
318                self.id.0
319            )));
320        }
321        self.logical_type.validate()
322    }
323}
324
325/// Assign deterministic, depth-first preorder field ids to a fresh field tree.
326pub(crate) fn assign_fresh_field_ids(fields: &mut [DataField], next_id: &mut u32) -> Result<()> {
327    for field in fields {
328        field.id = FieldId(*next_id);
329        *next_id = next_id.checked_add(1).ok_or_else(|| {
330            TableError::InvalidSchema("fresh schema exceeds available field ids".to_string())
331        })?;
332        assign_fresh_type_ids(&mut field.logical_type, next_id)?;
333    }
334    Ok(())
335}
336
337pub(crate) fn assign_fresh_type_ids(
338    logical_type: &mut LogicalType,
339    next_id: &mut u32,
340) -> Result<()> {
341    match &mut logical_type.kind {
342        LogicalTypeKind::List { element_type } => assign_fresh_type_ids(element_type, next_id),
343        LogicalTypeKind::Map {
344            key_type,
345            value_type,
346        } => {
347            assign_fresh_type_ids(key_type, next_id)?;
348            assign_fresh_type_ids(value_type, next_id)
349        }
350        LogicalTypeKind::Struct { fields } => assign_fresh_field_ids(fields, next_id),
351        LogicalTypeKind::Extension { extension } => {
352            assign_fresh_type_ids(&mut extension.physical_type, next_id)
353        }
354        _ => Ok(()),
355    }
356}