data_modelling_sdk/models/
column.rs

1//! Column model for the SDK
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Foreign key reference to another table's column
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct ForeignKey {
9    /// Target table ID (UUID as string)
10    pub table_id: String,
11    /// Column name in the target table
12    pub column_name: String,
13}
14
15/// ODCS v3.1.0 Relationship at property level
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct PropertyRelationship {
18    /// Relationship type (e.g., "foreignKey", "parent", "child")
19    #[serde(rename = "type")]
20    pub relationship_type: String,
21    /// Target reference (e.g., "definitions/order_id", "schema/id/properties/id")
22    pub to: String,
23}
24
25/// Column model representing a field in a table
26///
27/// A column defines a single field with a data type, constraints, and optional metadata.
28/// Columns can be primary keys, foreign keys, nullable, and have various constraints.
29///
30/// # Example
31///
32/// ```rust
33/// use data_modelling_sdk::models::Column;
34///
35/// let column = Column::new("id".to_string(), "INT".to_string());
36/// ```
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct Column {
39    /// Column name
40    pub name: String,
41    /// Data type / logical type (e.g., "number", "string", "integer")
42    /// For ODCS this maps to logicalType
43    pub data_type: String,
44    /// Physical type - the actual database type (e.g., "DOUBLE", "VARCHAR(100)", "BIGINT")
45    /// For ODCS this maps to physicalType. Optional as not all formats distinguish logical/physical types.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub physical_type: Option<String>,
48    /// Whether the column allows NULL values (default: true)
49    #[serde(default = "default_true")]
50    pub nullable: bool,
51    /// Whether this column is part of the primary key (default: false)
52    #[serde(default)]
53    pub primary_key: bool,
54    /// Whether this column is a secondary key (default: false)
55    #[serde(default)]
56    pub secondary_key: bool,
57    /// Composite key name if this column is part of a composite key
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub composite_key: Option<String>,
60    /// Foreign key reference if this column references another table
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub foreign_key: Option<ForeignKey>,
63    /// Additional constraints (e.g., "CHECK", "UNIQUE")
64    #[serde(default)]
65    pub constraints: Vec<String>,
66    /// Column description/documentation
67    #[serde(default)]
68    pub description: String,
69    /// Validation errors and warnings
70    #[serde(default)]
71    pub errors: Vec<HashMap<String, serde_json::Value>>,
72    /// Quality rules and checks
73    #[serde(default)]
74    pub quality: Vec<HashMap<String, serde_json::Value>>,
75    /// ODCS v3.1.0 relationships (property-level references)
76    /// Replaces the legacy $ref field - all $ref values are now converted to relationships on import
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub relationships: Vec<PropertyRelationship>,
79    /// Enum values if this column is an enumeration type
80    #[serde(default)]
81    pub enum_values: Vec<String>,
82    /// Display order for UI rendering
83    #[serde(default)]
84    pub column_order: i32,
85    /// Nested data type for ARRAY<STRUCT> or MAP types (overrides schema parsing)
86    #[serde(skip_serializing_if = "Option::is_none", rename = "nestedData")]
87    pub nested_data: Option<String>,
88}
89
90fn default_true() -> bool {
91    true
92}
93
94impl Column {
95    /// Create a new column with the given name and data type
96    ///
97    /// # Arguments
98    ///
99    /// * `name` - The column name (must be valid according to naming conventions)
100    /// * `data_type` - The data type string (e.g., "INT", "VARCHAR(100)")
101    ///
102    /// # Returns
103    ///
104    /// A new `Column` instance with default values (nullable=true, primary_key=false).
105    ///
106    /// # Example
107    ///
108    /// ```rust
109    /// use data_modelling_sdk::models::Column;
110    ///
111    /// let col = Column::new("user_id".to_string(), "BIGINT".to_string());
112    /// ```
113    #[allow(deprecated)]
114    pub fn new(name: String, data_type: String) -> Self {
115        Self {
116            name,
117            data_type: normalize_data_type(&data_type),
118            physical_type: None,
119            nullable: true,
120            primary_key: false,
121            secondary_key: false,
122            composite_key: None,
123            foreign_key: None,
124            constraints: Vec::new(),
125            description: String::new(),
126            errors: Vec::new(),
127            quality: Vec::new(),
128            relationships: Vec::new(),
129            enum_values: Vec::new(),
130            column_order: 0,
131            nested_data: None,
132        }
133    }
134}
135
136fn normalize_data_type(data_type: &str) -> String {
137    if data_type.is_empty() {
138        return data_type.to_string();
139    }
140
141    let upper = data_type.to_uppercase();
142
143    // Handle STRUCT<...>, ARRAY<...>, MAP<...> preserving inner content
144    if upper.starts_with("STRUCT") {
145        if let Some(start) = data_type.find('<')
146            && let Some(end) = data_type.rfind('>')
147        {
148            let inner = &data_type[start + 1..end];
149            return format!("STRUCT<{}>", inner);
150        }
151        return format!("STRUCT{}", &data_type[6..]);
152    } else if upper.starts_with("ARRAY") {
153        if let Some(start) = data_type.find('<')
154            && let Some(end) = data_type.rfind('>')
155        {
156            let inner = &data_type[start + 1..end];
157            return format!("ARRAY<{}>", inner);
158        }
159        return format!("ARRAY{}", &data_type[5..]);
160    } else if upper.starts_with("MAP") {
161        if let Some(start) = data_type.find('<')
162            && let Some(end) = data_type.rfind('>')
163        {
164            let inner = &data_type[start + 1..end];
165            return format!("MAP<{}>", inner);
166        }
167        return format!("MAP{}", &data_type[3..]);
168    }
169
170    upper
171}