data_modelling_core/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)]
8#[serde(rename_all = "camelCase")]
9pub struct ForeignKey {
10    /// Target table ID (UUID as string)
11    #[serde(alias = "table_id")]
12    pub table_id: String,
13    /// Column name in the target table
14    #[serde(alias = "column_name")]
15    pub column_name: String,
16}
17
18/// ODCS v3.1.0 Relationship at property level
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(rename_all = "camelCase")]
21pub struct PropertyRelationship {
22    /// Relationship type (e.g., "foreignKey", "parent", "child")
23    #[serde(rename = "type", alias = "relationship_type")]
24    pub relationship_type: String,
25    /// Target reference (e.g., "definitions/order_id", "schema/id/properties/id")
26    pub to: String,
27}
28
29/// ODCS v3.1.0 logicalTypeOptions for additional type metadata
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
31#[serde(rename_all = "camelCase")]
32pub struct LogicalTypeOptions {
33    /// Minimum length for strings
34    #[serde(skip_serializing_if = "Option::is_none", alias = "min_length")]
35    pub min_length: Option<i64>,
36    /// Maximum length for strings
37    #[serde(skip_serializing_if = "Option::is_none", alias = "max_length")]
38    pub max_length: Option<i64>,
39    /// Regex pattern for strings
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub pattern: Option<String>,
42    /// Format hint (e.g., "email", "uuid", "uri")
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub format: Option<String>,
45    /// Minimum value for numbers/dates
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub minimum: Option<serde_json::Value>,
48    /// Maximum value for numbers/dates
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub maximum: Option<serde_json::Value>,
51    /// Exclusive minimum for numbers
52    #[serde(skip_serializing_if = "Option::is_none", alias = "exclusive_minimum")]
53    pub exclusive_minimum: Option<serde_json::Value>,
54    /// Exclusive maximum for numbers
55    #[serde(skip_serializing_if = "Option::is_none", alias = "exclusive_maximum")]
56    pub exclusive_maximum: Option<serde_json::Value>,
57    /// Precision for decimals
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub precision: Option<i32>,
60    /// Scale for decimals
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub scale: Option<i32>,
63}
64
65impl LogicalTypeOptions {
66    pub fn is_empty(&self) -> bool {
67        self.min_length.is_none()
68            && self.max_length.is_none()
69            && self.pattern.is_none()
70            && self.format.is_none()
71            && self.minimum.is_none()
72            && self.maximum.is_none()
73            && self.exclusive_minimum.is_none()
74            && self.exclusive_maximum.is_none()
75            && self.precision.is_none()
76            && self.scale.is_none()
77    }
78}
79
80/// Authoritative definition reference (ODCS v3.1.0)
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
82#[serde(rename_all = "camelCase")]
83pub struct AuthoritativeDefinition {
84    /// Type of the reference (e.g., "businessDefinition", "transformationImplementation")
85    #[serde(rename = "type", alias = "definition_type")]
86    pub definition_type: String,
87    /// URL to the authoritative definition
88    pub url: String,
89}
90
91/// Column model representing a field in a table
92///
93/// A column defines a single field with a data type, constraints, and optional metadata.
94/// This model supports all ODCS v3.1.0 property fields to ensure no data loss during import/export.
95///
96/// # Example
97///
98/// ```rust
99/// use data_modelling_core::models::Column;
100///
101/// let column = Column::new("id".to_string(), "INT".to_string());
102/// ```
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104#[serde(rename_all = "camelCase")]
105pub struct Column {
106    // === Core Identity Fields ===
107    /// Stable technical identifier (ODCS: id)
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub id: Option<String>,
110    /// Column name (ODCS: name)
111    pub name: String,
112    /// Business name for the column (ODCS: businessName)
113    #[serde(skip_serializing_if = "Option::is_none", alias = "business_name")]
114    pub business_name: Option<String>,
115    /// Column description/documentation (ODCS: description)
116    #[serde(default)]
117    pub description: String,
118
119    // === Type Information ===
120    /// Logical data type (ODCS: logicalType - e.g., "string", "integer", "number")
121    #[serde(rename = "dataType", alias = "data_type")]
122    pub data_type: String,
123    /// Physical database type (ODCS: physicalType - e.g., "VARCHAR(100)", "BIGINT")
124    #[serde(skip_serializing_if = "Option::is_none", alias = "physical_type")]
125    pub physical_type: Option<String>,
126    /// Physical name in the data source (ODCS: physicalName)
127    #[serde(skip_serializing_if = "Option::is_none", alias = "physical_name")]
128    pub physical_name: Option<String>,
129    /// Additional type options (ODCS: logicalTypeOptions)
130    #[serde(
131        skip_serializing_if = "Option::is_none",
132        alias = "logical_type_options"
133    )]
134    pub logical_type_options: Option<LogicalTypeOptions>,
135
136    // === Key Constraints ===
137    /// Whether this column is part of the primary key (ODCS: primaryKey)
138    #[serde(default, alias = "primary_key")]
139    pub primary_key: bool,
140    /// Position in composite primary key, 1-based (ODCS: primaryKeyPosition)
141    #[serde(
142        skip_serializing_if = "Option::is_none",
143        alias = "primary_key_position"
144    )]
145    pub primary_key_position: Option<i32>,
146    /// Whether the column contains unique values (ODCS: unique)
147    #[serde(default)]
148    pub unique: bool,
149    /// Whether the column allows NULL values (inverse of ODCS: required)
150    #[serde(default = "default_true")]
151    pub nullable: bool,
152
153    // === Partitioning & Clustering ===
154    /// Whether the column is used for partitioning (ODCS: partitioned)
155    #[serde(default)]
156    pub partitioned: bool,
157    /// Position in partition key, 1-based (ODCS: partitionKeyPosition)
158    #[serde(
159        skip_serializing_if = "Option::is_none",
160        alias = "partition_key_position"
161    )]
162    pub partition_key_position: Option<i32>,
163    /// Whether the column is used for clustering
164    #[serde(default)]
165    pub clustered: bool,
166
167    // === Data Classification & Security ===
168    /// Data classification level (ODCS: classification - e.g., "confidential", "public")
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub classification: Option<String>,
171    /// Whether this is a critical data element (ODCS: criticalDataElement)
172    #[serde(default, alias = "critical_data_element")]
173    pub critical_data_element: bool,
174    /// Name of the encrypted version of this column (ODCS: encryptedName)
175    #[serde(skip_serializing_if = "Option::is_none", alias = "encrypted_name")]
176    pub encrypted_name: Option<String>,
177
178    // === Transformation Metadata ===
179    /// Source objects used in transformation (ODCS: transformSourceObjects)
180    #[serde(
181        default,
182        skip_serializing_if = "Vec::is_empty",
183        alias = "transform_source_objects"
184    )]
185    pub transform_source_objects: Vec<String>,
186    /// Transformation logic/expression (ODCS: transformLogic)
187    #[serde(skip_serializing_if = "Option::is_none", alias = "transform_logic")]
188    pub transform_logic: Option<String>,
189    /// Human-readable transformation description (ODCS: transformDescription)
190    #[serde(
191        skip_serializing_if = "Option::is_none",
192        alias = "transform_description"
193    )]
194    pub transform_description: Option<String>,
195
196    // === Examples & Documentation ===
197    /// Example values for this column (ODCS: examples)
198    #[serde(default, skip_serializing_if = "Vec::is_empty")]
199    pub examples: Vec<serde_json::Value>,
200    /// Default value for the column
201    #[serde(skip_serializing_if = "Option::is_none", alias = "default_value")]
202    pub default_value: Option<serde_json::Value>,
203
204    // === Relationships & References ===
205    /// ODCS v3.1.0 relationships (property-level references)
206    #[serde(default, skip_serializing_if = "Vec::is_empty")]
207    pub relationships: Vec<PropertyRelationship>,
208    /// Authoritative definitions (ODCS: authoritativeDefinitions)
209    #[serde(
210        default,
211        skip_serializing_if = "Vec::is_empty",
212        alias = "authoritative_definitions"
213    )]
214    pub authoritative_definitions: Vec<AuthoritativeDefinition>,
215
216    // === Quality & Validation ===
217    /// Quality rules and checks (ODCS: quality)
218    #[serde(default)]
219    pub quality: Vec<HashMap<String, serde_json::Value>>,
220    /// Enum values if this column is an enumeration type
221    #[serde(default, alias = "enum_values")]
222    pub enum_values: Vec<String>,
223
224    // === Tags & Custom Properties ===
225    /// Property-level tags (ODCS: tags)
226    #[serde(default, skip_serializing_if = "Vec::is_empty")]
227    pub tags: Vec<String>,
228    /// Custom properties for format-specific metadata not covered by ODCS
229    #[serde(
230        default,
231        skip_serializing_if = "HashMap::is_empty",
232        alias = "custom_properties"
233    )]
234    pub custom_properties: HashMap<String, serde_json::Value>,
235
236    // === Legacy/Internal Fields ===
237    /// Whether this column is a secondary/business key
238    #[serde(default, alias = "secondary_key")]
239    pub secondary_key: bool,
240    /// Composite key name if this column is part of a composite key
241    #[serde(skip_serializing_if = "Option::is_none", alias = "composite_key")]
242    pub composite_key: Option<String>,
243    /// Foreign key reference (legacy - prefer relationships)
244    #[serde(skip_serializing_if = "Option::is_none", alias = "foreign_key")]
245    pub foreign_key: Option<ForeignKey>,
246    /// Additional constraints (e.g., "CHECK", "UNIQUE")
247    #[serde(default)]
248    pub constraints: Vec<String>,
249    /// Validation errors and warnings
250    #[serde(default)]
251    pub errors: Vec<HashMap<String, serde_json::Value>>,
252    /// Display order for UI rendering
253    #[serde(default, alias = "column_order")]
254    pub column_order: i32,
255    /// Nested data type for ARRAY<STRUCT> or MAP types
256    #[serde(skip_serializing_if = "Option::is_none", alias = "nested_data")]
257    pub nested_data: Option<String>,
258}
259
260fn default_true() -> bool {
261    true
262}
263
264impl Default for Column {
265    fn default() -> Self {
266        Self {
267            // Core Identity
268            id: None,
269            name: String::new(),
270            business_name: None,
271            description: String::new(),
272            // Type Information
273            data_type: String::new(),
274            physical_type: None,
275            physical_name: None,
276            logical_type_options: None,
277            // Key Constraints
278            primary_key: false,
279            primary_key_position: None,
280            unique: false,
281            nullable: true, // Default to nullable
282            // Partitioning & Clustering
283            partitioned: false,
284            partition_key_position: None,
285            clustered: false,
286            // Data Classification & Security
287            classification: None,
288            critical_data_element: false,
289            encrypted_name: None,
290            // Transformation Metadata
291            transform_source_objects: Vec::new(),
292            transform_logic: None,
293            transform_description: None,
294            // Examples & Documentation
295            examples: Vec::new(),
296            default_value: None,
297            // Relationships & References
298            relationships: Vec::new(),
299            authoritative_definitions: Vec::new(),
300            // Quality & Validation
301            quality: Vec::new(),
302            enum_values: Vec::new(),
303            // Tags & Custom Properties
304            tags: Vec::new(),
305            custom_properties: HashMap::new(),
306            // Legacy/Internal Fields
307            secondary_key: false,
308            composite_key: None,
309            foreign_key: None,
310            constraints: Vec::new(),
311            errors: Vec::new(),
312            column_order: 0,
313            nested_data: None,
314        }
315    }
316}
317
318impl Column {
319    /// Create a new column with the given name and data type
320    ///
321    /// # Arguments
322    ///
323    /// * `name` - The column name (must be valid according to naming conventions)
324    /// * `data_type` - The data type string (e.g., "INT", "VARCHAR(100)")
325    ///
326    /// # Returns
327    ///
328    /// A new `Column` instance with default values (nullable=true, primary_key=false).
329    ///
330    /// # Example
331    ///
332    /// ```rust
333    /// use data_modelling_core::models::Column;
334    ///
335    /// let col = Column::new("user_id".to_string(), "BIGINT".to_string());
336    /// ```
337    #[allow(deprecated)]
338    pub fn new(name: String, data_type: String) -> Self {
339        Self {
340            // Core Identity
341            id: None,
342            name,
343            business_name: None,
344            description: String::new(),
345            // Type Information
346            data_type: normalize_data_type(&data_type),
347            physical_type: None,
348            physical_name: None,
349            logical_type_options: None,
350            // Key Constraints
351            primary_key: false,
352            primary_key_position: None,
353            unique: false,
354            nullable: true,
355            // Partitioning & Clustering
356            partitioned: false,
357            partition_key_position: None,
358            clustered: false,
359            // Data Classification & Security
360            classification: None,
361            critical_data_element: false,
362            encrypted_name: None,
363            // Transformation Metadata
364            transform_source_objects: Vec::new(),
365            transform_logic: None,
366            transform_description: None,
367            // Examples & Documentation
368            examples: Vec::new(),
369            default_value: None,
370            // Relationships & References
371            relationships: Vec::new(),
372            authoritative_definitions: Vec::new(),
373            // Quality & Validation
374            quality: Vec::new(),
375            enum_values: Vec::new(),
376            // Tags & Custom Properties
377            tags: Vec::new(),
378            custom_properties: HashMap::new(),
379            // Legacy/Internal Fields
380            secondary_key: false,
381            composite_key: None,
382            foreign_key: None,
383            constraints: Vec::new(),
384            errors: Vec::new(),
385            column_order: 0,
386            nested_data: None,
387        }
388    }
389}
390
391fn normalize_data_type(data_type: &str) -> String {
392    if data_type.is_empty() {
393        return data_type.to_string();
394    }
395
396    let upper = data_type.to_uppercase();
397
398    // Handle STRUCT<...>, ARRAY<...>, MAP<...> preserving inner content
399    if upper.starts_with("STRUCT") {
400        if let Some(start) = data_type.find('<')
401            && let Some(end) = data_type.rfind('>')
402        {
403            let inner = &data_type[start + 1..end];
404            return format!("STRUCT<{}>", inner);
405        }
406        return format!("STRUCT{}", &data_type[6..]);
407    } else if upper.starts_with("ARRAY") {
408        if let Some(start) = data_type.find('<')
409            && let Some(end) = data_type.rfind('>')
410        {
411            let inner = &data_type[start + 1..end];
412            return format!("ARRAY<{}>", inner);
413        }
414        return format!("ARRAY{}", &data_type[5..]);
415    } else if upper.starts_with("MAP") {
416        if let Some(start) = data_type.find('<')
417            && let Some(end) = data_type.rfind('>')
418        {
419            let inner = &data_type[start + 1..end];
420            return format!("MAP<{}>", inner);
421        }
422        return format!("MAP{}", &data_type[3..]);
423    }
424
425    upper
426}