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}