Skip to main content

data_modelling_core/export/
odcs.rs

1//! ODCS exporter for generating ODCS v3.1.0 YAML from data models.
2//!
3//! This module exports data models to ODCS (Open Data Contract Standard) v3.1.0 format only.
4//! Legacy ODCL formats are no longer supported for export.
5//!
6//! The exporter uses struct-based serialization via `ODCSContract` to ensure consistent
7//! key ordering in the YAML output, which produces stable git diffs.
8
9use super::{ExportError, ExportResult};
10use crate::models::odcs::ODCSContract;
11use crate::models::{Column, DataModel, Table};
12use serde_yaml;
13use std::collections::HashMap;
14
15/// Get the physical type for a column.
16/// Uses the dedicated physical_type field if available, otherwise falls back to data_type.
17fn get_physical_type(column: &Column) -> String {
18    column
19        .physical_type
20        .clone()
21        .unwrap_or_else(|| column.data_type.clone())
22}
23
24/// Exporter for ODCS (Open Data Contract Standard) v3.1.0 YAML format.
25pub struct ODCSExporter;
26
27impl ODCSExporter {
28    /// Export a table to ODCS v3.1.0 YAML format.
29    ///
30    /// Note: Only ODCS v3.1.0 format is supported. Legacy formats have been removed.
31    ///
32    /// # Arguments
33    ///
34    /// * `table` - The table to export
35    /// * `_format` - Format parameter (ignored, always uses ODCS v3.1.0)
36    ///
37    /// # Returns
38    ///
39    /// A YAML string in ODCS v3.1.0 format.
40    ///
41    /// # Example
42    ///
43    /// ```rust
44    /// use data_modelling_core::export::odcs::ODCSExporter;
45    /// use data_modelling_core::models::{Table, Column};
46    ///
47    /// let table = Table::new(
48    ///     "users".to_string(),
49    ///     vec![Column::new("id".to_string(), "BIGINT".to_string())],
50    /// );
51    ///
52    /// let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
53    /// assert!(yaml.contains("apiVersion: v3.1.0"));
54    /// assert!(yaml.contains("kind: DataContract"));
55    /// ```
56    pub fn export_table(table: &Table, _format: &str) -> String {
57        // Convert Table to ODCSContract using struct-based converter
58        // This ensures consistent key ordering in the YAML output
59        let contract = ODCSContract::from_table(table);
60        Self::export_contract(&contract)
61    }
62
63    /// Export an ODCSContract directly to YAML format.
64    ///
65    /// This is the preferred v2 API that directly serializes an `ODCSContract` struct,
66    /// preserving all metadata and nested structures without reconstruction.
67    ///
68    /// # Arguments
69    ///
70    /// * `contract` - The ODCSContract to export
71    ///
72    /// # Returns
73    ///
74    /// A YAML string in ODCS v3.1.0 format.
75    ///
76    /// # Example
77    ///
78    /// ```rust
79    /// use data_modelling_core::export::odcs::ODCSExporter;
80    /// use data_modelling_core::models::odcs::{ODCSContract, SchemaObject, Property};
81    ///
82    /// let contract = ODCSContract {
83    ///     api_version: "v3.1.0".to_string(),
84    ///     kind: "DataContract".to_string(),
85    ///     id: "my-contract-id".to_string(),
86    ///     name: "My Contract".to_string(),
87    ///     version: "1.0.0".to_string(),
88    ///     status: Some("active".to_string()),
89    ///     schema: vec![SchemaObject {
90    ///         name: "users".to_string(),
91    ///         properties: vec![Property {
92    ///             name: "id".to_string(),
93    ///             logical_type: "integer".to_string(),
94    ///             physical_type: Some("BIGINT".to_string()),
95    ///             required: true,
96    ///             primary_key: true,
97    ///             ..Default::default()
98    ///         }],
99    ///         ..Default::default()
100    ///     }],
101    ///     ..Default::default()
102    /// };
103    ///
104    /// let yaml = ODCSExporter::export_contract(&contract);
105    /// assert!(yaml.contains("apiVersion: v3.1.0"));
106    /// assert!(yaml.contains("kind: DataContract"));
107    /// ```
108    pub fn export_contract(contract: &crate::models::odcs::ODCSContract) -> String {
109        // Serialize the contract directly to YAML
110        // serde will handle the camelCase conversion via #[serde(rename_all = "camelCase")]
111        match serde_yaml::to_string(contract) {
112            Ok(yaml) => yaml,
113            Err(e) => {
114                // Fallback: return error as comment in YAML
115                format!("# Error serializing contract: {}\n", e)
116            }
117        }
118    }
119
120    /// Export an ODCSContract to YAML with validation.
121    ///
122    /// Similar to `export_contract()` but returns a Result for error handling.
123    ///
124    /// # Arguments
125    ///
126    /// * `contract` - The ODCSContract to export
127    ///
128    /// # Returns
129    ///
130    /// A Result containing the YAML string or an ExportError.
131    pub fn export_contract_validated(
132        contract: &crate::models::odcs::ODCSContract,
133    ) -> Result<String, ExportError> {
134        let yaml = serde_yaml::to_string(contract).map_err(|e| {
135            ExportError::SerializationError(format!("Failed to serialize contract: {}", e))
136        })?;
137
138        // Validate exported YAML against ODCS schema (if feature enabled)
139        #[cfg(feature = "schema-validation")]
140        {
141            use crate::validation::schema::validate_odcs_internal;
142            validate_odcs_internal(&yaml).map_err(|e| {
143                ExportError::ValidationError(format!("ODCS validation failed: {}", e))
144            })?;
145        }
146
147        Ok(yaml)
148    }
149
150    /// Parse STRUCT definition from data_type string and create nested properties
151    /// This is used when SQL parser doesn't create nested columns but we have STRUCT types
152    fn parse_struct_properties_from_data_type(
153        parent_name: &str,
154        data_type: &str,
155        map_data_type_fn: &dyn Fn(&str) -> (String, bool),
156    ) -> Option<Vec<serde_yaml::Value>> {
157        use crate::import::odcs::ODCSImporter;
158
159        let importer = ODCSImporter::new();
160        let field_data = serde_json::Map::new();
161
162        // Extract STRUCT definition from ARRAY<STRUCT<...>> if needed
163        let struct_type = if data_type.to_uppercase().starts_with("ARRAY<STRUCT<") {
164            // Extract the STRUCT<...> part from ARRAY<STRUCT<...>>
165            if let Some(start) = data_type.find("STRUCT<") {
166                &data_type[start..]
167            } else {
168                data_type
169            }
170        } else {
171            data_type
172        };
173
174        // Try to parse STRUCT type to get nested columns
175        if let Ok(nested_cols) =
176            importer.parse_struct_type_from_string(parent_name, struct_type, &field_data)
177            && !nested_cols.is_empty()
178        {
179            // Group nested columns by their immediate child name (handle nested STRUCTs)
180            // For ARRAY<STRUCT>, columns have names like: "parent.[].field" or "parent.[].nested.field"
181            // We need to group by immediate child: "field" vs "nested.field"
182            use std::collections::HashMap;
183            let mut props_map: HashMap<String, Vec<&crate::models::Column>> = HashMap::new();
184
185            for nested_col in &nested_cols {
186                // Extract the column name after removing parent prefix and array notation
187                let name_after_prefix = nested_col
188                    .name
189                    .strip_prefix(&format!("{}.[]", parent_name))
190                    .or_else(|| nested_col.name.strip_prefix(&format!("{}.", parent_name)))
191                    .unwrap_or(&nested_col.name);
192
193                // Get the immediate child name (first part before dot, if any)
194                let immediate_child = name_after_prefix
195                    .split('.')
196                    .next()
197                    .unwrap_or(name_after_prefix)
198                    .to_string();
199
200                props_map
201                    .entry(immediate_child)
202                    .or_default()
203                    .push(nested_col);
204            }
205
206            // Convert grouped columns to properties array format
207            let mut props_array = Vec::new();
208            for (immediate_child, child_cols) in props_map {
209                // Skip empty immediate_child names
210                if immediate_child.is_empty() {
211                    continue;
212                }
213
214                // Check if this is a nested STRUCT (has columns with dots after the immediate child)
215                let has_nested_struct = child_cols.iter().any(|col| {
216                    let name_after_prefix = col
217                        .name
218                        .strip_prefix(&format!("{}.[]", parent_name))
219                        .or_else(|| col.name.strip_prefix(&format!("{}.", parent_name)))
220                        .unwrap_or(&col.name);
221                    name_after_prefix.contains('.')
222                });
223
224                if has_nested_struct {
225                    // Nested STRUCT - create object property with nested properties
226                    let mut prop_map = serde_yaml::Mapping::new();
227                    let id = immediate_child
228                        .chars()
229                        .map(|c| {
230                            if c.is_alphanumeric() {
231                                c.to_lowercase().to_string()
232                            } else {
233                                "_".to_string()
234                            }
235                        })
236                        .collect::<String>()
237                        .replace("__", "_");
238
239                    prop_map.insert(
240                        serde_yaml::Value::String("id".to_string()),
241                        serde_yaml::Value::String(format!("{}_field", id)),
242                    );
243                    prop_map.insert(
244                        serde_yaml::Value::String("name".to_string()),
245                        serde_yaml::Value::String(immediate_child.clone()),
246                    );
247                    prop_map.insert(
248                        serde_yaml::Value::String("logicalType".to_string()),
249                        serde_yaml::Value::String("object".to_string()),
250                    );
251
252                    // Recursively build nested properties
253                    let mut nested_props = Vec::new();
254                    for nested_col in child_cols {
255                        // Remove the immediate child prefix to get the nested field name
256                        let name_after_prefix = nested_col
257                            .name
258                            .strip_prefix(&format!("{}.[]", parent_name))
259                            .or_else(|| nested_col.name.strip_prefix(&format!("{}.", parent_name)))
260                            .unwrap_or(&nested_col.name);
261
262                        let nested_name = name_after_prefix
263                            .strip_prefix(&format!("{}.", immediate_child))
264                            .unwrap_or(name_after_prefix)
265                            .to_string();
266
267                        if !nested_name.is_empty() && nested_name != immediate_child {
268                            let (logical_type, _) = map_data_type_fn(&nested_col.data_type);
269                            let nested_id = nested_name
270                                .chars()
271                                .map(|c| {
272                                    if c.is_alphanumeric() {
273                                        c.to_lowercase().to_string()
274                                    } else {
275                                        "_".to_string()
276                                    }
277                                })
278                                .collect::<String>()
279                                .replace("__", "_");
280
281                            let mut nested_prop = serde_yaml::Mapping::new();
282                            nested_prop.insert(
283                                serde_yaml::Value::String("id".to_string()),
284                                serde_yaml::Value::String(format!("{}_field", nested_id)),
285                            );
286                            nested_prop.insert(
287                                serde_yaml::Value::String("name".to_string()),
288                                serde_yaml::Value::String(nested_name),
289                            );
290                            nested_prop.insert(
291                                serde_yaml::Value::String("logicalType".to_string()),
292                                serde_yaml::Value::String(logical_type),
293                            );
294                            nested_prop.insert(
295                                serde_yaml::Value::String("physicalType".to_string()),
296                                serde_yaml::Value::String(get_physical_type(nested_col)),
297                            );
298
299                            if !nested_col.nullable {
300                                nested_prop.insert(
301                                    serde_yaml::Value::String("required".to_string()),
302                                    serde_yaml::Value::Bool(true),
303                                );
304                            }
305
306                            nested_props.push(serde_yaml::Value::Mapping(nested_prop));
307                        }
308                    }
309
310                    if !nested_props.is_empty() {
311                        prop_map.insert(
312                            serde_yaml::Value::String("properties".to_string()),
313                            serde_yaml::Value::Sequence(nested_props),
314                        );
315                    }
316
317                    props_array.push(serde_yaml::Value::Mapping(prop_map));
318                } else {
319                    // Simple field (no nested STRUCT)
320                    let nested_col = child_cols[0];
321                    let mut prop_map = serde_yaml::Mapping::new();
322                    let (logical_type, _) = map_data_type_fn(&nested_col.data_type);
323
324                    let id = immediate_child
325                        .chars()
326                        .map(|c| {
327                            if c.is_alphanumeric() {
328                                c.to_lowercase().to_string()
329                            } else {
330                                "_".to_string()
331                            }
332                        })
333                        .collect::<String>()
334                        .replace("__", "_");
335
336                    prop_map.insert(
337                        serde_yaml::Value::String("id".to_string()),
338                        serde_yaml::Value::String(format!("{}_field", id)),
339                    );
340                    prop_map.insert(
341                        serde_yaml::Value::String("name".to_string()),
342                        serde_yaml::Value::String(immediate_child),
343                    );
344                    prop_map.insert(
345                        serde_yaml::Value::String("logicalType".to_string()),
346                        serde_yaml::Value::String(logical_type),
347                    );
348                    prop_map.insert(
349                        serde_yaml::Value::String("physicalType".to_string()),
350                        serde_yaml::Value::String(get_physical_type(nested_col)),
351                    );
352
353                    if !nested_col.nullable {
354                        prop_map.insert(
355                            serde_yaml::Value::String("required".to_string()),
356                            serde_yaml::Value::Bool(true),
357                        );
358                    }
359
360                    if !nested_col.description.is_empty() {
361                        prop_map.insert(
362                            serde_yaml::Value::String("description".to_string()),
363                            serde_yaml::Value::String(nested_col.description.clone()),
364                        );
365                    }
366
367                    props_array.push(serde_yaml::Value::Mapping(prop_map));
368                }
369            }
370            return Some(props_array);
371        }
372        None
373    }
374
375    /// Map data type to ODCS logicalType
376    /// Returns (logical_type, is_array)
377    fn map_data_type_to_logical_type(data_type: &str) -> (String, bool) {
378        let upper = data_type.to_uppercase();
379
380        // Check for array types first
381        if upper.starts_with("ARRAY<") {
382            return ("array".to_string(), true);
383        }
384
385        // Map to ODCS logical types
386        if upper.contains("INT") || upper == "BIGINT" || upper == "SMALLINT" || upper == "TINYINT" {
387            ("integer".to_string(), false)
388        } else if upper.contains("DECIMAL")
389            || upper.contains("DOUBLE")
390            || upper.contains("FLOAT")
391            || upper.contains("NUMERIC")
392            || upper == "NUMBER"
393        {
394            ("number".to_string(), false)
395        } else if upper == "BOOLEAN" || upper == "BOOL" {
396            ("boolean".to_string(), false)
397        } else if upper == "DATE" {
398            ("date".to_string(), false)
399        } else if upper.contains("TIMESTAMP") {
400            ("timestamp".to_string(), false)
401        } else if upper == "TIME" {
402            ("time".to_string(), false)
403        } else if upper == "STRUCT" || upper == "OBJECT" || upper.starts_with("STRUCT<") {
404            ("object".to_string(), false)
405        } else {
406            // Default to string for VARCHAR, CHAR, STRING, TEXT, etc.
407            ("string".to_string(), false)
408        }
409    }
410
411    /// Helper to convert serde_json::Value to serde_yaml::Value
412    fn json_to_yaml_value(json: &serde_json::Value) -> serde_yaml::Value {
413        match json {
414            serde_json::Value::Null => serde_yaml::Value::Null,
415            serde_json::Value::Bool(b) => serde_yaml::Value::Bool(*b),
416            serde_json::Value::Number(n) => {
417                if let Some(i) = n.as_i64() {
418                    serde_yaml::Value::Number(serde_yaml::Number::from(i))
419                } else if let Some(f) = n.as_f64() {
420                    serde_yaml::Value::Number(serde_yaml::Number::from(f))
421                } else {
422                    serde_yaml::Value::String(n.to_string())
423                }
424            }
425            serde_json::Value::String(s) => serde_yaml::Value::String(s.clone()),
426            serde_json::Value::Array(arr) => {
427                let yaml_arr: Vec<serde_yaml::Value> =
428                    arr.iter().map(Self::json_to_yaml_value).collect();
429                serde_yaml::Value::Sequence(yaml_arr)
430            }
431            serde_json::Value::Object(obj) => {
432                let mut yaml_map = serde_yaml::Mapping::new();
433                for (k, v) in obj {
434                    yaml_map.insert(
435                        serde_yaml::Value::String(k.clone()),
436                        Self::json_to_yaml_value(v),
437                    );
438                }
439                serde_yaml::Value::Mapping(yaml_map)
440            }
441        }
442    }
443
444    /// Export in ODCS v3.1.0 format (the only supported export format).
445    fn export_odcs_v3_1_0_format(table: &Table) -> String {
446        let mut yaml = serde_yaml::Mapping::new();
447
448        // Required ODCS v3.1.0 fields
449        yaml.insert(
450            serde_yaml::Value::String("apiVersion".to_string()),
451            serde_yaml::Value::String("v3.1.0".to_string()),
452        );
453        yaml.insert(
454            serde_yaml::Value::String("kind".to_string()),
455            serde_yaml::Value::String("DataContract".to_string()),
456        );
457
458        // ID - use table UUID (ODCS spec: "A unique identifier used to reduce the risk of dataset name collisions, such as a UUID.")
459        yaml.insert(
460            serde_yaml::Value::String("id".to_string()),
461            serde_yaml::Value::String(table.id.to_string()),
462        );
463
464        // Name
465        yaml.insert(
466            serde_yaml::Value::String("name".to_string()),
467            serde_yaml::Value::String(table.name.clone()),
468        );
469
470        // Version - from metadata or default
471        let version = table
472            .odcl_metadata
473            .get("version")
474            .and_then(|v| v.as_str())
475            .map(|s| s.to_string())
476            .unwrap_or_else(|| "1.0.0".to_string());
477        yaml.insert(
478            serde_yaml::Value::String("version".to_string()),
479            serde_yaml::Value::String(version),
480        );
481
482        // Status - from metadata or default to "draft" (required field in ODCS v3.1.0)
483        let status_value = table
484            .odcl_metadata
485            .get("status")
486            .and_then(|v| {
487                if v.is_null() {
488                    None
489                } else {
490                    Some(Self::json_to_yaml_value(v))
491                }
492            })
493            .unwrap_or_else(|| serde_yaml::Value::String("draft".to_string()));
494        yaml.insert(
495            serde_yaml::Value::String("status".to_string()),
496            status_value,
497        );
498
499        // Domain - from metadata
500        if let Some(domain) = table.odcl_metadata.get("domain")
501            && !domain.is_null()
502        {
503            yaml.insert(
504                serde_yaml::Value::String("domain".to_string()),
505                Self::json_to_yaml_value(domain),
506            );
507        }
508
509        // Data Product - from metadata
510        if let Some(data_product) = table.odcl_metadata.get("dataProduct")
511            && !data_product.is_null()
512        {
513            yaml.insert(
514                serde_yaml::Value::String("dataProduct".to_string()),
515                Self::json_to_yaml_value(data_product),
516            );
517        }
518
519        // Tenant - from metadata
520        if let Some(tenant) = table.odcl_metadata.get("tenant")
521            && !tenant.is_null()
522        {
523            yaml.insert(
524                serde_yaml::Value::String("tenant".to_string()),
525                Self::json_to_yaml_value(tenant),
526            );
527        }
528
529        // Description - from metadata (can be object or string)
530        if let Some(description) = table.odcl_metadata.get("description")
531            && !description.is_null()
532        {
533            yaml.insert(
534                serde_yaml::Value::String("description".to_string()),
535                Self::json_to_yaml_value(description),
536            );
537        }
538
539        // Tags
540        if !table.tags.is_empty() {
541            let tags_yaml: Vec<serde_yaml::Value> = table
542                .tags
543                .iter()
544                .map(|t| serde_yaml::Value::String(t.to_string()))
545                .collect();
546            yaml.insert(
547                serde_yaml::Value::String("tags".to_string()),
548                serde_yaml::Value::Sequence(tags_yaml),
549            );
550        }
551
552        // Team - from metadata
553        if let Some(team) = table.odcl_metadata.get("team")
554            && !team.is_null()
555        {
556            yaml.insert(
557                serde_yaml::Value::String("team".to_string()),
558                Self::json_to_yaml_value(team),
559            );
560        }
561
562        // Roles - from metadata
563        if let Some(roles) = table.odcl_metadata.get("roles")
564            && !roles.is_null()
565        {
566            yaml.insert(
567                serde_yaml::Value::String("roles".to_string()),
568                Self::json_to_yaml_value(roles),
569            );
570        }
571
572        // Pricing - from metadata (ODCS uses "price")
573        if let Some(pricing) = table.odcl_metadata.get("pricing")
574            && !pricing.is_null()
575        {
576            yaml.insert(
577                serde_yaml::Value::String("price".to_string()),
578                Self::json_to_yaml_value(pricing),
579            );
580        }
581
582        // Terms - from metadata
583        if let Some(terms) = table.odcl_metadata.get("terms")
584            && !terms.is_null()
585        {
586            yaml.insert(
587                serde_yaml::Value::String("terms".to_string()),
588                Self::json_to_yaml_value(terms),
589            );
590        }
591
592        // Servers - from metadata
593        if let Some(servers) = table.odcl_metadata.get("servers")
594            && !servers.is_null()
595        {
596            yaml.insert(
597                serde_yaml::Value::String("servers".to_string()),
598                Self::json_to_yaml_value(servers),
599            );
600        }
601
602        // Service Levels - from metadata
603        if let Some(servicelevels) = table.odcl_metadata.get("servicelevels")
604            && !servicelevels.is_null()
605        {
606            yaml.insert(
607                serde_yaml::Value::String("servicelevels".to_string()),
608                Self::json_to_yaml_value(servicelevels),
609            );
610        }
611
612        // Links - from metadata
613        if let Some(links) = table.odcl_metadata.get("links")
614            && !links.is_null()
615        {
616            yaml.insert(
617                serde_yaml::Value::String("links".to_string()),
618                Self::json_to_yaml_value(links),
619            );
620        }
621
622        // Infrastructure - from metadata
623        if let Some(infrastructure) = table.odcl_metadata.get("infrastructure")
624            && !infrastructure.is_null()
625        {
626            yaml.insert(
627                serde_yaml::Value::String("infrastructure".to_string()),
628                Self::json_to_yaml_value(infrastructure),
629            );
630        }
631
632        // Schema array (ODCS v3.1.0 uses array of SchemaObject)
633        let mut schema_array = Vec::new();
634        let mut schema_obj = serde_yaml::Mapping::new();
635
636        schema_obj.insert(
637            serde_yaml::Value::String("name".to_string()),
638            serde_yaml::Value::String(table.name.clone()),
639        );
640
641        // Build properties from columns (ODCS v3.1.0 uses array format)
642        let mut properties = Vec::new();
643
644        // Helper function to convert Mapping properties to Array format (ODCS v3.1.0)
645        fn mapping_to_properties_array(props_map: serde_yaml::Mapping) -> Vec<serde_yaml::Value> {
646            let mut props_array = Vec::new();
647            for (key, value) in props_map {
648                if let serde_yaml::Value::String(name) = key
649                    && let serde_yaml::Value::Mapping(mut prop_map) = value
650                {
651                    // Add 'name' field to property object (required in ODCS v3.1.0)
652                    prop_map.insert(
653                        serde_yaml::Value::String("name".to_string()),
654                        serde_yaml::Value::String(name.clone()),
655                    );
656                    props_array.push(serde_yaml::Value::Mapping(prop_map));
657                }
658            }
659            props_array
660        }
661
662        // Helper function to build nested properties structure (returns array format for ODCS v3.1.0)
663        fn build_nested_properties(
664            parent_name: &str,
665            _table_name: &str,
666            all_columns: &[crate::models::Column],
667            json_to_yaml_fn: &dyn Fn(&serde_json::Value) -> serde_yaml::Value,
668            map_data_type_fn: &dyn Fn(&str) -> (String, bool),
669        ) -> Option<Vec<serde_yaml::Value>> {
670            // Handle both dot notation (parent.field) and array notation (parent.[].field)
671            let parent_prefix_dot = format!("{}.", parent_name);
672            let parent_prefix_array = format!("{}.[].", parent_name); // Include trailing dot for proper stripping
673            let parent_prefix_array_no_dot = format!("{}.[]", parent_name); // For filtering
674            let nested_columns: Vec<&crate::models::Column> = all_columns
675                .iter()
676                .filter(|col| {
677                    col.name.starts_with(&parent_prefix_dot)
678                        || col.name.starts_with(&parent_prefix_array_no_dot)
679                })
680                .collect();
681
682            if nested_columns.is_empty() {
683                return None;
684            }
685
686            let mut nested_props_map = serde_yaml::Mapping::new();
687
688            // Group nested columns by their immediate child name (first level only)
689            let mut child_map: std::collections::HashMap<String, Vec<&crate::models::Column>> =
690                std::collections::HashMap::new();
691
692            for nested_col in &nested_columns {
693                // Handle both dot notation (parent.field) and array notation (parent.[].field)
694                // Also handle deeply nested structures like parent.[].nested.[].field
695                let relative_name = if nested_col.name.starts_with(&parent_prefix_array) {
696                    // For array notation: parent.[].field -> field
697                    // Or parent.[].nested.[].field -> nested.[].field (for nested arrays)
698                    nested_col
699                        .name
700                        .strip_prefix(&parent_prefix_array)
701                        .unwrap_or("")
702                } else if nested_col.name.starts_with(&parent_prefix_dot) {
703                    // For dot notation: parent.field -> field
704                    // Or parent.nested.field -> nested.field (for nested structs)
705                    nested_col
706                        .name
707                        .strip_prefix(&parent_prefix_dot)
708                        .unwrap_or("")
709                } else {
710                    continue;
711                };
712
713                if relative_name.is_empty() {
714                    continue;
715                }
716
717                // Find the immediate child name (first level only)
718                // For "nested.[].field" -> "nested"
719                // For "nested.field" -> "nested"
720                // For "field" -> "field"
721                let child_name = if let Some(dot_pos) = relative_name.find('.') {
722                    // Check if it's array notation (.[]) or regular dot notation
723                    if relative_name.starts_with(".[]") {
724                        // This shouldn't happen - array notation should be at parent level
725                        // But handle it just in case: ".[].field" -> skip
726                        continue;
727                    } else {
728                        // Regular dot: "nested.field" -> "nested"
729                        &relative_name[..dot_pos]
730                    }
731                } else {
732                    // Direct child: "field" -> "field"
733                    relative_name
734                };
735
736                child_map
737                    .entry(child_name.to_string())
738                    .or_default()
739                    .push(nested_col);
740            }
741
742            // Build properties for each child
743            for (child_name, child_cols) in child_map {
744                // Skip empty child names
745                if child_name.is_empty() {
746                    continue;
747                }
748                // Find the direct child column (first level only, may have deeper nesting)
749                // For ARRAY<STRUCT<...>>, we don't have a parent column - only nested columns exist
750                // So we need to find a column that matches the child_name exactly or starts with child_name.
751                let direct_child = child_cols.iter().find(|col| {
752                    let rel_name = if col.name.starts_with(&parent_prefix_array) {
753                        col.name.strip_prefix(&parent_prefix_array)
754                    } else if col.name.starts_with(&parent_prefix_dot) {
755                        col.name.strip_prefix(&parent_prefix_dot)
756                    } else {
757                        None
758                    };
759                    // Match if relative name equals child_name or starts with child_name followed by . or .[]
760                    rel_name
761                        .map(|rel| {
762                            rel == child_name
763                                || rel.starts_with(&format!("{}.", child_name))
764                                || rel.starts_with(&format!("{}.[]", child_name))
765                        })
766                        .unwrap_or(false)
767                });
768
769                // Check if this child has nested children
770                let child_has_nested = child_cols.iter().any(|col| {
771                    let rel_name = if col.name.starts_with(&parent_prefix_array) {
772                        col.name.strip_prefix(&parent_prefix_array)
773                    } else if col.name.starts_with(&parent_prefix_dot) {
774                        col.name.strip_prefix(&parent_prefix_dot)
775                    } else {
776                        None
777                    };
778                    rel_name
779                        .map(|rel| {
780                            rel.starts_with(&format!("{}.", child_name)) && rel != child_name
781                        })
782                        .unwrap_or(false)
783                });
784
785                // Find the direct child column
786                // For ARRAY<STRUCT<...>>, we might not have a parent column, only nested columns
787                // direct_child should already find columns where rel == child_name or starts with child_name.
788                // If it's None, find a column where the relative name equals child_name exactly
789                let child_col = if let Some(col) = direct_child {
790                    Some(*col)
791                } else {
792                    // Find a column where the relative name equals child_name exactly
793                    child_cols
794                        .iter()
795                        .find(|col| {
796                            let rel_name = if col.name.starts_with(&parent_prefix_array) {
797                                col.name.strip_prefix(&parent_prefix_array)
798                            } else if col.name.starts_with(&parent_prefix_dot) {
799                                col.name.strip_prefix(&parent_prefix_dot)
800                            } else {
801                                None
802                            };
803                            rel_name.map(|rel| rel == child_name).unwrap_or(false)
804                        })
805                        .copied()
806                };
807
808                if let Some(child_col) = child_col {
809                    let mut child_prop = serde_yaml::Mapping::new();
810
811                    // Add the name field (required in ODCS v3.1.0) - add it first
812                    child_prop.insert(
813                        serde_yaml::Value::String("name".to_string()),
814                        serde_yaml::Value::String(child_name.clone()),
815                    );
816
817                    // Handle ARRAY<OBJECT> or ARRAY<STRUCT> types
818                    let data_type_upper = child_col.data_type.to_uppercase();
819                    let is_array_object = data_type_upper.starts_with("ARRAY<")
820                        && (data_type_upper.contains("OBJECT")
821                            || data_type_upper.contains("STRUCT"));
822                    let is_struct_or_object = data_type_upper == "STRUCT"
823                        || data_type_upper == "OBJECT"
824                        || data_type_upper.starts_with("STRUCT<");
825
826                    // Try to build nested properties first (regardless of type)
827                    let nested_props_array = build_nested_properties(
828                        &child_col.name,
829                        _table_name,
830                        all_columns,
831                        json_to_yaml_fn,
832                        map_data_type_fn,
833                    );
834
835                    if is_array_object && (child_has_nested || nested_props_array.is_some()) {
836                        // ARRAY<OBJECT> with nested fields
837                        child_prop.insert(
838                            serde_yaml::Value::String("logicalType".to_string()),
839                            serde_yaml::Value::String("array".to_string()),
840                        );
841                        child_prop.insert(
842                            serde_yaml::Value::String("physicalType".to_string()),
843                            serde_yaml::Value::String(get_physical_type(child_col)),
844                        );
845
846                        let mut items = serde_yaml::Mapping::new();
847                        items.insert(
848                            serde_yaml::Value::String("logicalType".to_string()),
849                            serde_yaml::Value::String("object".to_string()),
850                        );
851
852                        // Add nested properties if they exist (already in array format)
853                        if let Some(nested_props) = nested_props_array {
854                            items.insert(
855                                serde_yaml::Value::String("properties".to_string()),
856                                serde_yaml::Value::Sequence(nested_props),
857                            );
858                        }
859
860                        child_prop.insert(
861                            serde_yaml::Value::String("items".to_string()),
862                            serde_yaml::Value::Mapping(items),
863                        );
864                    } else if is_struct_or_object
865                        || child_has_nested
866                        || nested_props_array.is_some()
867                    {
868                        // OBJECT/STRUCT with nested properties, or any column with nested children
869                        child_prop.insert(
870                            serde_yaml::Value::String("logicalType".to_string()),
871                            serde_yaml::Value::String("object".to_string()),
872                        );
873                        child_prop.insert(
874                            serde_yaml::Value::String("physicalType".to_string()),
875                            serde_yaml::Value::String(get_physical_type(child_col)),
876                        );
877
878                        // Add nested properties if they exist (already in array format)
879                        if let Some(nested_props) = nested_props_array {
880                            child_prop.insert(
881                                serde_yaml::Value::String("properties".to_string()),
882                                serde_yaml::Value::Sequence(nested_props),
883                            );
884                        }
885                    } else {
886                        // Simple field
887                        let (logical_type, _) = map_data_type_fn(&child_col.data_type);
888                        child_prop.insert(
889                            serde_yaml::Value::String("logicalType".to_string()),
890                            serde_yaml::Value::String(logical_type),
891                        );
892                        child_prop.insert(
893                            serde_yaml::Value::String("physicalType".to_string()),
894                            serde_yaml::Value::String(get_physical_type(child_col)),
895                        );
896                    }
897
898                    if !child_col.nullable {
899                        child_prop.insert(
900                            serde_yaml::Value::String("required".to_string()),
901                            serde_yaml::Value::Bool(true),
902                        );
903                    }
904
905                    if !child_col.description.is_empty() {
906                        child_prop.insert(
907                            serde_yaml::Value::String("description".to_string()),
908                            serde_yaml::Value::String(child_col.description.clone()),
909                        );
910                    }
911
912                    // Export column-level quality rules for nested columns
913                    if !child_col.quality.is_empty() {
914                        let quality_array: Vec<serde_yaml::Value> = child_col
915                            .quality
916                            .iter()
917                            .map(|rule| {
918                                let mut rule_map = serde_yaml::Mapping::new();
919                                for (k, v) in rule {
920                                    rule_map.insert(
921                                        serde_yaml::Value::String(k.clone()),
922                                        json_to_yaml_fn(v),
923                                    );
924                                }
925                                serde_yaml::Value::Mapping(rule_map)
926                            })
927                            .collect();
928                        child_prop.insert(
929                            serde_yaml::Value::String("quality".to_string()),
930                            serde_yaml::Value::Sequence(quality_array),
931                        );
932                    }
933
934                    // Export relationships array for nested columns (ODCS v3.1.0 format)
935                    if !child_col.relationships.is_empty() {
936                        let rels_yaml: Vec<serde_yaml::Value> = child_col
937                            .relationships
938                            .iter()
939                            .map(|rel| {
940                                let mut rel_map = serde_yaml::Mapping::new();
941                                rel_map.insert(
942                                    serde_yaml::Value::String("type".to_string()),
943                                    serde_yaml::Value::String(rel.relationship_type.clone()),
944                                );
945                                rel_map.insert(
946                                    serde_yaml::Value::String("to".to_string()),
947                                    serde_yaml::Value::String(rel.to.clone()),
948                                );
949                                serde_yaml::Value::Mapping(rel_map)
950                            })
951                            .collect();
952                        child_prop.insert(
953                            serde_yaml::Value::String("relationships".to_string()),
954                            serde_yaml::Value::Sequence(rels_yaml),
955                        );
956                    }
957
958                    // Convert enum values to ODCS quality rules for nested columns
959                    // ODCS v3.1.0 doesn't support 'enum' field in properties - use quality rules instead
960                    if !child_col.enum_values.is_empty() {
961                        let quality = child_prop
962                            .entry(serde_yaml::Value::String("quality".to_string()))
963                            .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
964
965                        if let serde_yaml::Value::Sequence(quality_rules) = quality {
966                            let mut enum_rule = serde_yaml::Mapping::new();
967                            enum_rule.insert(
968                                serde_yaml::Value::String("type".to_string()),
969                                serde_yaml::Value::String("sql".to_string()),
970                            );
971
972                            let enum_list: String = child_col
973                                .enum_values
974                                .iter()
975                                .map(|e| format!("'{}'", e.replace('\'', "''")))
976                                .collect::<Vec<_>>()
977                                .join(", ");
978                            let query = format!(
979                                "SELECT COUNT(*) FROM ${{table}} WHERE ${{column}} NOT IN ({})",
980                                enum_list
981                            );
982
983                            enum_rule.insert(
984                                serde_yaml::Value::String("query".to_string()),
985                                serde_yaml::Value::String(query),
986                            );
987
988                            enum_rule.insert(
989                                serde_yaml::Value::String("mustBe".to_string()),
990                                serde_yaml::Value::Number(serde_yaml::Number::from(0)),
991                            );
992
993                            enum_rule.insert(
994                                serde_yaml::Value::String("description".to_string()),
995                                serde_yaml::Value::String(format!(
996                                    "Value must be one of: {}",
997                                    child_col.enum_values.join(", ")
998                                )),
999                            );
1000
1001                            quality_rules.push(serde_yaml::Value::Mapping(enum_rule));
1002                        }
1003                    }
1004
1005                    // Export constraints for nested columns
1006                    if !child_col.constraints.is_empty() {
1007                        let constraints_yaml: Vec<serde_yaml::Value> = child_col
1008                            .constraints
1009                            .iter()
1010                            .map(|c| serde_yaml::Value::String(c.clone()))
1011                            .collect();
1012                        child_prop.insert(
1013                            serde_yaml::Value::String("constraints".to_string()),
1014                            serde_yaml::Value::Sequence(constraints_yaml),
1015                        );
1016                    }
1017
1018                    // Export foreign key for nested columns
1019                    if let Some(ref fk) = child_col.foreign_key {
1020                        let mut fk_map = serde_yaml::Mapping::new();
1021                        fk_map.insert(
1022                            serde_yaml::Value::String("table".to_string()),
1023                            serde_yaml::Value::String(fk.table_id.clone()),
1024                        );
1025                        fk_map.insert(
1026                            serde_yaml::Value::String("column".to_string()),
1027                            serde_yaml::Value::String(fk.column_name.clone()),
1028                        );
1029                        child_prop.insert(
1030                            serde_yaml::Value::String("foreignKey".to_string()),
1031                            serde_yaml::Value::Mapping(fk_map),
1032                        );
1033                    }
1034
1035                    // === Additional Column Metadata Fields for Nested Columns ===
1036
1037                    // businessName
1038                    if let Some(ref biz_name) = child_col.business_name {
1039                        child_prop.insert(
1040                            serde_yaml::Value::String("businessName".to_string()),
1041                            serde_yaml::Value::String(biz_name.clone()),
1042                        );
1043                    }
1044
1045                    // physicalName
1046                    if let Some(ref phys_name) = child_col.physical_name {
1047                        child_prop.insert(
1048                            serde_yaml::Value::String("physicalName".to_string()),
1049                            serde_yaml::Value::String(phys_name.clone()),
1050                        );
1051                    }
1052
1053                    // unique
1054                    if child_col.unique {
1055                        child_prop.insert(
1056                            serde_yaml::Value::String("unique".to_string()),
1057                            serde_yaml::Value::Bool(true),
1058                        );
1059                    }
1060
1061                    // primaryKey
1062                    if child_col.primary_key {
1063                        child_prop.insert(
1064                            serde_yaml::Value::String("primaryKey".to_string()),
1065                            serde_yaml::Value::Bool(true),
1066                        );
1067                    }
1068
1069                    // primaryKeyPosition
1070                    if let Some(pk_pos) = child_col.primary_key_position {
1071                        child_prop.insert(
1072                            serde_yaml::Value::String("primaryKeyPosition".to_string()),
1073                            serde_yaml::Value::Number(serde_yaml::Number::from(pk_pos)),
1074                        );
1075                    }
1076
1077                    // partitioned
1078                    if child_col.partitioned {
1079                        child_prop.insert(
1080                            serde_yaml::Value::String("partitioned".to_string()),
1081                            serde_yaml::Value::Bool(true),
1082                        );
1083                    }
1084
1085                    // partitionKeyPosition
1086                    if let Some(pos) = child_col.partition_key_position {
1087                        child_prop.insert(
1088                            serde_yaml::Value::String("partitionKeyPosition".to_string()),
1089                            serde_yaml::Value::Number(serde_yaml::Number::from(pos)),
1090                        );
1091                    }
1092
1093                    // clustered
1094                    if child_col.clustered {
1095                        child_prop.insert(
1096                            serde_yaml::Value::String("clustered".to_string()),
1097                            serde_yaml::Value::Bool(true),
1098                        );
1099                    }
1100
1101                    // classification
1102                    if let Some(ref class) = child_col.classification {
1103                        child_prop.insert(
1104                            serde_yaml::Value::String("classification".to_string()),
1105                            serde_yaml::Value::String(class.clone()),
1106                        );
1107                    }
1108
1109                    // criticalDataElement
1110                    if child_col.critical_data_element {
1111                        child_prop.insert(
1112                            serde_yaml::Value::String("criticalDataElement".to_string()),
1113                            serde_yaml::Value::Bool(true),
1114                        );
1115                    }
1116
1117                    // encryptedName
1118                    if let Some(ref enc) = child_col.encrypted_name {
1119                        child_prop.insert(
1120                            serde_yaml::Value::String("encryptedName".to_string()),
1121                            serde_yaml::Value::String(enc.clone()),
1122                        );
1123                    }
1124
1125                    // transformSourceObjects
1126                    if !child_col.transform_source_objects.is_empty() {
1127                        let sources: Vec<serde_yaml::Value> = child_col
1128                            .transform_source_objects
1129                            .iter()
1130                            .map(|s| serde_yaml::Value::String(s.clone()))
1131                            .collect();
1132                        child_prop.insert(
1133                            serde_yaml::Value::String("transformSourceObjects".to_string()),
1134                            serde_yaml::Value::Sequence(sources),
1135                        );
1136                    }
1137
1138                    // transformLogic
1139                    if let Some(ref logic) = child_col.transform_logic {
1140                        child_prop.insert(
1141                            serde_yaml::Value::String("transformLogic".to_string()),
1142                            serde_yaml::Value::String(logic.clone()),
1143                        );
1144                    }
1145
1146                    // transformDescription
1147                    if let Some(ref desc) = child_col.transform_description {
1148                        child_prop.insert(
1149                            serde_yaml::Value::String("transformDescription".to_string()),
1150                            serde_yaml::Value::String(desc.clone()),
1151                        );
1152                    }
1153
1154                    // examples
1155                    if !child_col.examples.is_empty() {
1156                        let examples: Vec<serde_yaml::Value> =
1157                            child_col.examples.iter().map(json_to_yaml_fn).collect();
1158                        child_prop.insert(
1159                            serde_yaml::Value::String("examples".to_string()),
1160                            serde_yaml::Value::Sequence(examples),
1161                        );
1162                    }
1163
1164                    // authoritativeDefinitions
1165                    if !child_col.authoritative_definitions.is_empty() {
1166                        let defs: Vec<serde_yaml::Value> = child_col
1167                            .authoritative_definitions
1168                            .iter()
1169                            .map(|d| {
1170                                let mut def_map = serde_yaml::Mapping::new();
1171                                def_map.insert(
1172                                    serde_yaml::Value::String("type".to_string()),
1173                                    serde_yaml::Value::String(d.definition_type.clone()),
1174                                );
1175                                def_map.insert(
1176                                    serde_yaml::Value::String("url".to_string()),
1177                                    serde_yaml::Value::String(d.url.clone()),
1178                                );
1179                                serde_yaml::Value::Mapping(def_map)
1180                            })
1181                            .collect();
1182                        child_prop.insert(
1183                            serde_yaml::Value::String("authoritativeDefinitions".to_string()),
1184                            serde_yaml::Value::Sequence(defs),
1185                        );
1186                    }
1187
1188                    // tags
1189                    if !child_col.tags.is_empty() {
1190                        let tags: Vec<serde_yaml::Value> = child_col
1191                            .tags
1192                            .iter()
1193                            .map(|t| serde_yaml::Value::String(t.clone()))
1194                            .collect();
1195                        child_prop.insert(
1196                            serde_yaml::Value::String("tags".to_string()),
1197                            serde_yaml::Value::Sequence(tags),
1198                        );
1199                    }
1200
1201                    // customProperties
1202                    if !child_col.custom_properties.is_empty() {
1203                        let props: Vec<serde_yaml::Value> = child_col
1204                            .custom_properties
1205                            .iter()
1206                            .map(|(k, v)| {
1207                                let mut m = serde_yaml::Mapping::new();
1208                                m.insert(
1209                                    serde_yaml::Value::String("property".to_string()),
1210                                    serde_yaml::Value::String(k.clone()),
1211                                );
1212                                m.insert(
1213                                    serde_yaml::Value::String("value".to_string()),
1214                                    json_to_yaml_fn(v),
1215                                );
1216                                serde_yaml::Value::Mapping(m)
1217                            })
1218                            .collect();
1219                        child_prop.insert(
1220                            serde_yaml::Value::String("customProperties".to_string()),
1221                            serde_yaml::Value::Sequence(props),
1222                        );
1223                    }
1224
1225                    // businessKey (secondary_key)
1226                    if child_col.secondary_key {
1227                        child_prop.insert(
1228                            serde_yaml::Value::String("businessKey".to_string()),
1229                            serde_yaml::Value::Bool(true),
1230                        );
1231                    }
1232
1233                    nested_props_map.insert(
1234                        serde_yaml::Value::String(child_name.clone()),
1235                        serde_yaml::Value::Mapping(child_prop),
1236                    );
1237                } else if !child_cols.is_empty() {
1238                    // No exact match found, but we have columns for this child_name
1239                    // Use the first column that matches child_name exactly (should be the direct child)
1240                    if let Some(first_col) = child_cols.iter().find(|col| {
1241                        let rel_name = if col.name.starts_with(&parent_prefix_array) {
1242                            col.name.strip_prefix(&parent_prefix_array)
1243                        } else if col.name.starts_with(&parent_prefix_dot) {
1244                            col.name.strip_prefix(&parent_prefix_dot)
1245                        } else {
1246                            None
1247                        };
1248                        rel_name.map(|rel| rel == child_name).unwrap_or(false)
1249                    }) {
1250                        let mut child_prop = serde_yaml::Mapping::new();
1251
1252                        // Add the name field (required in ODCS v3.1.0)
1253                        child_prop.insert(
1254                            serde_yaml::Value::String("name".to_string()),
1255                            serde_yaml::Value::String(child_name.clone()),
1256                        );
1257
1258                        // Check if this child has nested children
1259                        let child_has_nested = child_cols.iter().any(|col| {
1260                            let rel_name = if col.name.starts_with(&parent_prefix_array) {
1261                                col.name.strip_prefix(&parent_prefix_array)
1262                            } else if col.name.starts_with(&parent_prefix_dot) {
1263                                col.name.strip_prefix(&parent_prefix_dot)
1264                            } else {
1265                                None
1266                            };
1267                            rel_name
1268                                .map(|rel| {
1269                                    rel.starts_with(&format!("{}.", child_name))
1270                                        && rel != child_name
1271                                })
1272                                .unwrap_or(false)
1273                        });
1274
1275                        // Try to build nested properties if there are nested children
1276                        let nested_props_array = if child_has_nested {
1277                            build_nested_properties(
1278                                &first_col.name,
1279                                _table_name,
1280                                all_columns,
1281                                json_to_yaml_fn,
1282                                map_data_type_fn,
1283                            )
1284                        } else {
1285                            None
1286                        };
1287
1288                        let data_type_upper = first_col.data_type.to_uppercase();
1289                        let is_array_object = data_type_upper.starts_with("ARRAY<")
1290                            && (data_type_upper.contains("OBJECT")
1291                                || data_type_upper.contains("STRUCT"));
1292                        let is_struct_or_object = data_type_upper == "STRUCT"
1293                            || data_type_upper == "OBJECT"
1294                            || data_type_upper.starts_with("STRUCT<");
1295
1296                        if is_array_object && (child_has_nested || nested_props_array.is_some()) {
1297                            child_prop.insert(
1298                                serde_yaml::Value::String("logicalType".to_string()),
1299                                serde_yaml::Value::String("array".to_string()),
1300                            );
1301                            child_prop.insert(
1302                                serde_yaml::Value::String("physicalType".to_string()),
1303                                serde_yaml::Value::String(get_physical_type(first_col)),
1304                            );
1305
1306                            let mut items = serde_yaml::Mapping::new();
1307                            items.insert(
1308                                serde_yaml::Value::String("logicalType".to_string()),
1309                                serde_yaml::Value::String("object".to_string()),
1310                            );
1311
1312                            if let Some(nested_props) = nested_props_array {
1313                                items.insert(
1314                                    serde_yaml::Value::String("properties".to_string()),
1315                                    serde_yaml::Value::Sequence(nested_props),
1316                                );
1317                            }
1318
1319                            child_prop.insert(
1320                                serde_yaml::Value::String("items".to_string()),
1321                                serde_yaml::Value::Mapping(items),
1322                            );
1323                        } else if is_struct_or_object
1324                            || child_has_nested
1325                            || nested_props_array.is_some()
1326                        {
1327                            child_prop.insert(
1328                                serde_yaml::Value::String("logicalType".to_string()),
1329                                serde_yaml::Value::String("object".to_string()),
1330                            );
1331                            child_prop.insert(
1332                                serde_yaml::Value::String("physicalType".to_string()),
1333                                serde_yaml::Value::String(get_physical_type(first_col)),
1334                            );
1335
1336                            if let Some(nested_props) = nested_props_array {
1337                                child_prop.insert(
1338                                    serde_yaml::Value::String("properties".to_string()),
1339                                    serde_yaml::Value::Sequence(nested_props),
1340                                );
1341                            }
1342                        } else {
1343                            let (logical_type, _) = map_data_type_fn(&first_col.data_type);
1344                            child_prop.insert(
1345                                serde_yaml::Value::String("logicalType".to_string()),
1346                                serde_yaml::Value::String(logical_type),
1347                            );
1348                            child_prop.insert(
1349                                serde_yaml::Value::String("physicalType".to_string()),
1350                                serde_yaml::Value::String(get_physical_type(first_col)),
1351                            );
1352                        }
1353
1354                        if !first_col.nullable {
1355                            child_prop.insert(
1356                                serde_yaml::Value::String("required".to_string()),
1357                                serde_yaml::Value::Bool(true),
1358                            );
1359                        }
1360
1361                        if !first_col.description.is_empty() {
1362                            child_prop.insert(
1363                                serde_yaml::Value::String("description".to_string()),
1364                                serde_yaml::Value::String(first_col.description.clone()),
1365                            );
1366                        }
1367
1368                        // === Additional Column Metadata Fields for Nested Columns ===
1369
1370                        // businessName
1371                        if let Some(ref biz_name) = first_col.business_name {
1372                            child_prop.insert(
1373                                serde_yaml::Value::String("businessName".to_string()),
1374                                serde_yaml::Value::String(biz_name.clone()),
1375                            );
1376                        }
1377
1378                        // physicalName
1379                        if let Some(ref phys_name) = first_col.physical_name {
1380                            child_prop.insert(
1381                                serde_yaml::Value::String("physicalName".to_string()),
1382                                serde_yaml::Value::String(phys_name.clone()),
1383                            );
1384                        }
1385
1386                        // unique
1387                        if first_col.unique {
1388                            child_prop.insert(
1389                                serde_yaml::Value::String("unique".to_string()),
1390                                serde_yaml::Value::Bool(true),
1391                            );
1392                        }
1393
1394                        // primaryKey
1395                        if first_col.primary_key {
1396                            child_prop.insert(
1397                                serde_yaml::Value::String("primaryKey".to_string()),
1398                                serde_yaml::Value::Bool(true),
1399                            );
1400                        }
1401
1402                        // primaryKeyPosition
1403                        if let Some(pk_pos) = first_col.primary_key_position {
1404                            child_prop.insert(
1405                                serde_yaml::Value::String("primaryKeyPosition".to_string()),
1406                                serde_yaml::Value::Number(serde_yaml::Number::from(pk_pos)),
1407                            );
1408                        }
1409
1410                        // partitioned
1411                        if first_col.partitioned {
1412                            child_prop.insert(
1413                                serde_yaml::Value::String("partitioned".to_string()),
1414                                serde_yaml::Value::Bool(true),
1415                            );
1416                        }
1417
1418                        // partitionKeyPosition
1419                        if let Some(pos) = first_col.partition_key_position {
1420                            child_prop.insert(
1421                                serde_yaml::Value::String("partitionKeyPosition".to_string()),
1422                                serde_yaml::Value::Number(serde_yaml::Number::from(pos)),
1423                            );
1424                        }
1425
1426                        // clustered
1427                        if first_col.clustered {
1428                            child_prop.insert(
1429                                serde_yaml::Value::String("clustered".to_string()),
1430                                serde_yaml::Value::Bool(true),
1431                            );
1432                        }
1433
1434                        // classification
1435                        if let Some(ref class) = first_col.classification {
1436                            child_prop.insert(
1437                                serde_yaml::Value::String("classification".to_string()),
1438                                serde_yaml::Value::String(class.clone()),
1439                            );
1440                        }
1441
1442                        // criticalDataElement
1443                        if first_col.critical_data_element {
1444                            child_prop.insert(
1445                                serde_yaml::Value::String("criticalDataElement".to_string()),
1446                                serde_yaml::Value::Bool(true),
1447                            );
1448                        }
1449
1450                        // encryptedName
1451                        if let Some(ref enc) = first_col.encrypted_name {
1452                            child_prop.insert(
1453                                serde_yaml::Value::String("encryptedName".to_string()),
1454                                serde_yaml::Value::String(enc.clone()),
1455                            );
1456                        }
1457
1458                        // transformSourceObjects
1459                        if !first_col.transform_source_objects.is_empty() {
1460                            let sources: Vec<serde_yaml::Value> = first_col
1461                                .transform_source_objects
1462                                .iter()
1463                                .map(|s| serde_yaml::Value::String(s.clone()))
1464                                .collect();
1465                            child_prop.insert(
1466                                serde_yaml::Value::String("transformSourceObjects".to_string()),
1467                                serde_yaml::Value::Sequence(sources),
1468                            );
1469                        }
1470
1471                        // transformLogic
1472                        if let Some(ref logic) = first_col.transform_logic {
1473                            child_prop.insert(
1474                                serde_yaml::Value::String("transformLogic".to_string()),
1475                                serde_yaml::Value::String(logic.clone()),
1476                            );
1477                        }
1478
1479                        // transformDescription
1480                        if let Some(ref desc) = first_col.transform_description {
1481                            child_prop.insert(
1482                                serde_yaml::Value::String("transformDescription".to_string()),
1483                                serde_yaml::Value::String(desc.clone()),
1484                            );
1485                        }
1486
1487                        // examples
1488                        if !first_col.examples.is_empty() {
1489                            let examples: Vec<serde_yaml::Value> =
1490                                first_col.examples.iter().map(json_to_yaml_fn).collect();
1491                            child_prop.insert(
1492                                serde_yaml::Value::String("examples".to_string()),
1493                                serde_yaml::Value::Sequence(examples),
1494                            );
1495                        }
1496
1497                        // authoritativeDefinitions
1498                        if !first_col.authoritative_definitions.is_empty() {
1499                            let defs: Vec<serde_yaml::Value> = first_col
1500                                .authoritative_definitions
1501                                .iter()
1502                                .map(|d| {
1503                                    let mut def_map = serde_yaml::Mapping::new();
1504                                    def_map.insert(
1505                                        serde_yaml::Value::String("type".to_string()),
1506                                        serde_yaml::Value::String(d.definition_type.clone()),
1507                                    );
1508                                    def_map.insert(
1509                                        serde_yaml::Value::String("url".to_string()),
1510                                        serde_yaml::Value::String(d.url.clone()),
1511                                    );
1512                                    serde_yaml::Value::Mapping(def_map)
1513                                })
1514                                .collect();
1515                            child_prop.insert(
1516                                serde_yaml::Value::String("authoritativeDefinitions".to_string()),
1517                                serde_yaml::Value::Sequence(defs),
1518                            );
1519                        }
1520
1521                        // tags
1522                        if !first_col.tags.is_empty() {
1523                            let tags: Vec<serde_yaml::Value> = first_col
1524                                .tags
1525                                .iter()
1526                                .map(|t| serde_yaml::Value::String(t.clone()))
1527                                .collect();
1528                            child_prop.insert(
1529                                serde_yaml::Value::String("tags".to_string()),
1530                                serde_yaml::Value::Sequence(tags),
1531                            );
1532                        }
1533
1534                        // customProperties
1535                        if !first_col.custom_properties.is_empty() {
1536                            let props: Vec<serde_yaml::Value> = first_col
1537                                .custom_properties
1538                                .iter()
1539                                .map(|(k, v)| {
1540                                    let mut m = serde_yaml::Mapping::new();
1541                                    m.insert(
1542                                        serde_yaml::Value::String("property".to_string()),
1543                                        serde_yaml::Value::String(k.clone()),
1544                                    );
1545                                    m.insert(
1546                                        serde_yaml::Value::String("value".to_string()),
1547                                        json_to_yaml_fn(v),
1548                                    );
1549                                    serde_yaml::Value::Mapping(m)
1550                                })
1551                                .collect();
1552                            child_prop.insert(
1553                                serde_yaml::Value::String("customProperties".to_string()),
1554                                serde_yaml::Value::Sequence(props),
1555                            );
1556                        }
1557
1558                        // businessKey (secondary_key)
1559                        if first_col.secondary_key {
1560                            child_prop.insert(
1561                                serde_yaml::Value::String("businessKey".to_string()),
1562                                serde_yaml::Value::Bool(true),
1563                            );
1564                        }
1565
1566                        nested_props_map.insert(
1567                            serde_yaml::Value::String(child_name.clone()),
1568                            serde_yaml::Value::Mapping(child_prop),
1569                        );
1570                    }
1571                } else {
1572                    // No columns found for this child - skip
1573                    continue;
1574                }
1575            }
1576
1577            if nested_props_map.is_empty() {
1578                None
1579            } else {
1580                // Convert Mapping to Array format (ODCS v3.1.0)
1581                Some(mapping_to_properties_array(nested_props_map))
1582            }
1583        }
1584
1585        for column in &table.columns {
1586            // Skip nested columns (they're handled as part of parent columns)
1587            if column.name.contains('.') {
1588                continue;
1589            }
1590
1591            let mut prop = serde_yaml::Mapping::new();
1592
1593            // Check if this column has nested columns
1594            // Handle both dot notation (parent.field) and array notation (parent.[].field)
1595            // Also handle deeply nested structures like parent.[].nested.[].field
1596            let column_prefix_dot = format!("{}.", column.name);
1597            let column_prefix_array = format!("{}.[]", column.name);
1598            let has_nested = table.columns.iter().any(|col| {
1599                (col.name.starts_with(&column_prefix_dot)
1600                    || col.name.starts_with(&column_prefix_array))
1601                    && col.name != column.name
1602            });
1603
1604            // Determine the type - handle ARRAY, STRUCT, OBJECT, etc.
1605            let data_type_upper = column.data_type.to_uppercase();
1606
1607            // Check if this is ARRAY<STRUCT> - can be detected in two ways:
1608            // 1. data_type == "ARRAY" with full type in description after ||
1609            // 2. data_type == "ARRAY" or "ARRAY<OBJECT>" with nested columns using .[] notation
1610            let is_array_struct_from_desc =
1611                data_type_upper == "ARRAY" && column.description.contains("|| ARRAY<STRUCT<");
1612            let is_array_struct_from_nested = (data_type_upper == "ARRAY"
1613                || data_type_upper == "ARRAY<OBJECT>")
1614                && has_nested
1615                && table
1616                    .columns
1617                    .iter()
1618                    .any(|col| col.name.starts_with(&format!("{}.[]", column.name)));
1619
1620            // Also check if data_type is ARRAY<STRING> but description contains ARRAY<STRUCT< (from SQL export)
1621            let is_array_struct_from_string_type = data_type_upper == "ARRAY<STRING>"
1622                && column.description.contains("|| ARRAY<STRUCT<");
1623
1624            let is_array_struct = is_array_struct_from_desc
1625                || is_array_struct_from_nested
1626                || is_array_struct_from_string_type;
1627
1628            // Extract the full ARRAY<STRUCT<...>> type from description if present
1629            let full_array_struct_type =
1630                if is_array_struct_from_desc || is_array_struct_from_string_type {
1631                    column
1632                        .description
1633                        .split("|| ")
1634                        .nth(1)
1635                        .map(|s| s.trim().to_string())
1636                } else if is_array_struct_from_nested {
1637                    // Reconstruct the STRUCT type from nested columns
1638                    // We'll use build_nested_properties to get the structure, then reconstruct the type string
1639                    None // Will be handled by parsing nested columns
1640                } else {
1641                    None
1642                };
1643
1644            let is_array_object = data_type_upper.starts_with("ARRAY<")
1645                && (data_type_upper.contains("OBJECT") || data_type_upper.contains("STRUCT"));
1646            let is_struct_or_object = data_type_upper == "STRUCT"
1647                || data_type_upper == "OBJECT"
1648                || data_type_upper.starts_with("STRUCT<");
1649
1650            // Handle ARRAY<STRUCT> types first - parse from description field or nested columns
1651            if is_array_struct {
1652                let struct_props = if let Some(ref full_type) = full_array_struct_type {
1653                    // Parse the STRUCT definition from the full type string (from SQL import)
1654                    Self::parse_struct_properties_from_data_type(
1655                        &column.name,
1656                        full_type,
1657                        &Self::map_data_type_to_logical_type,
1658                    )
1659                } else if is_array_struct_from_nested {
1660                    // Build properties from nested columns (from ODCS import)
1661                    build_nested_properties(
1662                        &column.name,
1663                        &table.name,
1664                        &table.columns,
1665                        &Self::json_to_yaml_value,
1666                        &Self::map_data_type_to_logical_type,
1667                    )
1668                } else {
1669                    None
1670                };
1671
1672                if let Some(props) = struct_props {
1673                    // Create array of objects structure
1674                    prop.insert(
1675                        serde_yaml::Value::String("logicalType".to_string()),
1676                        serde_yaml::Value::String("array".to_string()),
1677                    );
1678                    prop.insert(
1679                        serde_yaml::Value::String("physicalType".to_string()),
1680                        serde_yaml::Value::String("ARRAY".to_string()),
1681                    );
1682
1683                    let mut items = serde_yaml::Mapping::new();
1684                    items.insert(
1685                        serde_yaml::Value::String("logicalType".to_string()),
1686                        serde_yaml::Value::String("object".to_string()),
1687                    );
1688                    items.insert(
1689                        serde_yaml::Value::String("properties".to_string()),
1690                        serde_yaml::Value::Sequence(props),
1691                    );
1692
1693                    prop.insert(
1694                        serde_yaml::Value::String("items".to_string()),
1695                        serde_yaml::Value::Mapping(items),
1696                    );
1697
1698                    // Extract base description (before ||) if present
1699                    if is_array_struct_from_desc {
1700                        let base_description =
1701                            column.description.split("||").next().unwrap_or("").trim();
1702                        if !base_description.is_empty() {
1703                            prop.insert(
1704                                serde_yaml::Value::String("description".to_string()),
1705                                serde_yaml::Value::String(base_description.to_string()),
1706                            );
1707                        }
1708                    } else if !column.description.is_empty() {
1709                        // Use full description if not from SQL import
1710                        prop.insert(
1711                            serde_yaml::Value::String("description".to_string()),
1712                            serde_yaml::Value::String(column.description.clone()),
1713                        );
1714                    }
1715                } else {
1716                    // Fallback: if parsing fails, just use simple array type
1717                    prop.insert(
1718                        serde_yaml::Value::String("logicalType".to_string()),
1719                        serde_yaml::Value::String("array".to_string()),
1720                    );
1721                    prop.insert(
1722                        serde_yaml::Value::String("physicalType".to_string()),
1723                        serde_yaml::Value::String("ARRAY".to_string()),
1724                    );
1725                }
1726            }
1727            // Always check for nested properties if nested columns exist
1728            // Also handle STRUCT columns even if nested columns weren't created by SQL parser
1729            else if has_nested {
1730                // Try to build nested properties first
1731                let nested_props = build_nested_properties(
1732                    &column.name,
1733                    &table.name,
1734                    &table.columns,
1735                    &Self::json_to_yaml_value,
1736                    &Self::map_data_type_to_logical_type,
1737                );
1738
1739                if is_array_object {
1740                    // ARRAY<OBJECT> with nested fields
1741                    prop.insert(
1742                        serde_yaml::Value::String("logicalType".to_string()),
1743                        serde_yaml::Value::String("array".to_string()),
1744                    );
1745                    prop.insert(
1746                        serde_yaml::Value::String("physicalType".to_string()),
1747                        serde_yaml::Value::String(get_physical_type(column)),
1748                    );
1749
1750                    let mut items = serde_yaml::Mapping::new();
1751                    items.insert(
1752                        serde_yaml::Value::String("logicalType".to_string()),
1753                        serde_yaml::Value::String("object".to_string()),
1754                    );
1755
1756                    // Add nested properties if they exist (already in array format)
1757                    if let Some(nested_props_array) = nested_props {
1758                        items.insert(
1759                            serde_yaml::Value::String("properties".to_string()),
1760                            serde_yaml::Value::Sequence(nested_props_array),
1761                        );
1762                    }
1763
1764                    prop.insert(
1765                        serde_yaml::Value::String("items".to_string()),
1766                        serde_yaml::Value::Mapping(items),
1767                    );
1768                } else if is_struct_or_object || nested_props.is_some() {
1769                    // OBJECT/STRUCT with nested fields, or any column with nested columns
1770                    prop.insert(
1771                        serde_yaml::Value::String("logicalType".to_string()),
1772                        serde_yaml::Value::String("object".to_string()),
1773                    );
1774                    prop.insert(
1775                        serde_yaml::Value::String("physicalType".to_string()),
1776                        serde_yaml::Value::String(get_physical_type(column)),
1777                    );
1778
1779                    // Add nested properties if they exist (already in array format)
1780                    if let Some(nested_props_array) = nested_props {
1781                        prop.insert(
1782                            serde_yaml::Value::String("properties".to_string()),
1783                            serde_yaml::Value::Sequence(nested_props_array),
1784                        );
1785                    }
1786                } else {
1787                    // Has nested columns but couldn't build structure - use simple type
1788                    // But if it's ARRAY<OBJECT> with nested columns, still export as ARRAY with items
1789                    if is_array_object && has_nested {
1790                        // Even if build_nested_properties failed, try to create a simple array structure
1791                        prop.insert(
1792                            serde_yaml::Value::String("logicalType".to_string()),
1793                            serde_yaml::Value::String("array".to_string()),
1794                        );
1795                        prop.insert(
1796                            serde_yaml::Value::String("physicalType".to_string()),
1797                            serde_yaml::Value::String("ARRAY".to_string()),
1798                        );
1799
1800                        let mut items = serde_yaml::Mapping::new();
1801                        items.insert(
1802                            serde_yaml::Value::String("logicalType".to_string()),
1803                            serde_yaml::Value::String("object".to_string()),
1804                        );
1805
1806                        // Try build_nested_properties one more time - it might work now
1807                        if let Some(nested_props_array) = build_nested_properties(
1808                            &column.name,
1809                            &table.name,
1810                            &table.columns,
1811                            &Self::json_to_yaml_value,
1812                            &Self::map_data_type_to_logical_type,
1813                        ) {
1814                            items.insert(
1815                                serde_yaml::Value::String("properties".to_string()),
1816                                serde_yaml::Value::Sequence(nested_props_array),
1817                            );
1818                        }
1819
1820                        prop.insert(
1821                            serde_yaml::Value::String("items".to_string()),
1822                            serde_yaml::Value::Mapping(items),
1823                        );
1824                    } else {
1825                        let (logical_type, _) =
1826                            Self::map_data_type_to_logical_type(&column.data_type);
1827                        prop.insert(
1828                            serde_yaml::Value::String("logicalType".to_string()),
1829                            serde_yaml::Value::String(logical_type),
1830                        );
1831                        prop.insert(
1832                            serde_yaml::Value::String("physicalType".to_string()),
1833                            serde_yaml::Value::String(get_physical_type(column)),
1834                        );
1835                    }
1836                }
1837            } else if is_struct_or_object {
1838                // STRUCT/OBJECT type but no nested columns (e.g., from SQL parser that didn't create nested columns)
1839                // Try to parse STRUCT definition from data_type to create nested properties
1840                let parsed_props = Self::parse_struct_properties_from_data_type(
1841                    &column.name,
1842                    &column.data_type,
1843                    &Self::map_data_type_to_logical_type,
1844                );
1845
1846                prop.insert(
1847                    serde_yaml::Value::String("logicalType".to_string()),
1848                    serde_yaml::Value::String("object".to_string()),
1849                );
1850                prop.insert(
1851                    serde_yaml::Value::String("physicalType".to_string()),
1852                    serde_yaml::Value::String(get_physical_type(column)),
1853                );
1854
1855                // Add parsed nested properties if available
1856                if let Some(nested_props_array) = parsed_props {
1857                    prop.insert(
1858                        serde_yaml::Value::String("properties".to_string()),
1859                        serde_yaml::Value::Sequence(nested_props_array),
1860                    );
1861                }
1862            } else if prop.is_empty() {
1863                // No nested columns and prop is empty - use simple type
1864                let (logical_type, _) = Self::map_data_type_to_logical_type(&column.data_type);
1865                prop.insert(
1866                    serde_yaml::Value::String("logicalType".to_string()),
1867                    serde_yaml::Value::String(logical_type),
1868                );
1869                prop.insert(
1870                    serde_yaml::Value::String("physicalType".to_string()),
1871                    serde_yaml::Value::String(get_physical_type(column)),
1872                );
1873            }
1874            // If prop is not empty (was set by is_array_struct or has_nested blocks), use it as-is
1875
1876            if !column.nullable {
1877                prop.insert(
1878                    serde_yaml::Value::String("required".to_string()),
1879                    serde_yaml::Value::Bool(true),
1880                );
1881            }
1882
1883            if column.primary_key {
1884                prop.insert(
1885                    serde_yaml::Value::String("primaryKey".to_string()),
1886                    serde_yaml::Value::Bool(true),
1887                );
1888            }
1889
1890            if column.secondary_key {
1891                prop.insert(
1892                    serde_yaml::Value::String("businessKey".to_string()),
1893                    serde_yaml::Value::Bool(true),
1894                );
1895            }
1896
1897            if !column.description.is_empty() {
1898                prop.insert(
1899                    serde_yaml::Value::String("description".to_string()),
1900                    serde_yaml::Value::String(column.description.clone()),
1901                );
1902            }
1903
1904            // Export column-level quality rules
1905            if !column.quality.is_empty() {
1906                let quality_array: Vec<serde_yaml::Value> = column
1907                    .quality
1908                    .iter()
1909                    .map(|rule| {
1910                        let mut rule_map = serde_yaml::Mapping::new();
1911                        for (k, v) in rule {
1912                            rule_map.insert(
1913                                serde_yaml::Value::String(k.clone()),
1914                                Self::json_to_yaml_value(v),
1915                            );
1916                        }
1917                        serde_yaml::Value::Mapping(rule_map)
1918                    })
1919                    .collect();
1920                prop.insert(
1921                    serde_yaml::Value::String("quality".to_string()),
1922                    serde_yaml::Value::Sequence(quality_array),
1923                );
1924            }
1925
1926            // Export relationships array (ODCS v3.1.0 format)
1927            if !column.relationships.is_empty() {
1928                let rels_yaml: Vec<serde_yaml::Value> = column
1929                    .relationships
1930                    .iter()
1931                    .map(|rel| {
1932                        let mut rel_map = serde_yaml::Mapping::new();
1933                        rel_map.insert(
1934                            serde_yaml::Value::String("type".to_string()),
1935                            serde_yaml::Value::String(rel.relationship_type.clone()),
1936                        );
1937                        rel_map.insert(
1938                            serde_yaml::Value::String("to".to_string()),
1939                            serde_yaml::Value::String(rel.to.clone()),
1940                        );
1941                        serde_yaml::Value::Mapping(rel_map)
1942                    })
1943                    .collect();
1944                prop.insert(
1945                    serde_yaml::Value::String("relationships".to_string()),
1946                    serde_yaml::Value::Sequence(rels_yaml),
1947                );
1948            }
1949
1950            // Convert enum values to ODCS quality rules
1951            // ODCS v3.1.0 doesn't support 'enum' field in properties - use quality rules instead
1952            // Use SQL type with IN clause to validate enum values
1953            if !column.enum_values.is_empty() {
1954                // Check if there's already a quality array, if not create one
1955                let quality = prop
1956                    .entry(serde_yaml::Value::String("quality".to_string()))
1957                    .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
1958
1959                if let serde_yaml::Value::Sequence(quality_rules) = quality {
1960                    // Create a SQL quality rule for enum values
1961                    // Use SQL type with IN clause to validate enum values
1962                    let mut enum_rule = serde_yaml::Mapping::new();
1963                    enum_rule.insert(
1964                        serde_yaml::Value::String("type".to_string()),
1965                        serde_yaml::Value::String("sql".to_string()),
1966                    );
1967
1968                    // Build SQL query with IN clause for enum values
1969                    let enum_list: String = column
1970                        .enum_values
1971                        .iter()
1972                        .map(|e| format!("'{}'", e.replace('\'', "''"))) // Escape single quotes
1973                        .collect::<Vec<_>>()
1974                        .join(", ");
1975                    let query = format!(
1976                        "SELECT COUNT(*) FROM ${{table}} WHERE ${{column}} NOT IN ({})",
1977                        enum_list
1978                    );
1979
1980                    enum_rule.insert(
1981                        serde_yaml::Value::String("query".to_string()),
1982                        serde_yaml::Value::String(query),
1983                    );
1984
1985                    enum_rule.insert(
1986                        serde_yaml::Value::String("mustBe".to_string()),
1987                        serde_yaml::Value::Number(serde_yaml::Number::from(0)),
1988                    );
1989
1990                    enum_rule.insert(
1991                        serde_yaml::Value::String("description".to_string()),
1992                        serde_yaml::Value::String(format!(
1993                            "Value must be one of: {}",
1994                            column.enum_values.join(", ")
1995                        )),
1996                    );
1997
1998                    quality_rules.push(serde_yaml::Value::Mapping(enum_rule));
1999                }
2000            }
2001
2002            // Export constraints
2003            if !column.constraints.is_empty() {
2004                let constraints_yaml: Vec<serde_yaml::Value> = column
2005                    .constraints
2006                    .iter()
2007                    .map(|c| serde_yaml::Value::String(c.clone()))
2008                    .collect();
2009                prop.insert(
2010                    serde_yaml::Value::String("constraints".to_string()),
2011                    serde_yaml::Value::Sequence(constraints_yaml),
2012                );
2013            }
2014
2015            // === Additional ODCS v3.1.0 Column Fields ===
2016
2017            // businessName
2018            if let Some(ref biz_name) = column.business_name {
2019                prop.insert(
2020                    serde_yaml::Value::String("businessName".to_string()),
2021                    serde_yaml::Value::String(biz_name.clone()),
2022                );
2023            }
2024
2025            // physicalName
2026            if let Some(ref phys_name) = column.physical_name {
2027                prop.insert(
2028                    serde_yaml::Value::String("physicalName".to_string()),
2029                    serde_yaml::Value::String(phys_name.clone()),
2030                );
2031            }
2032
2033            // logicalTypeOptions
2034            if let Some(ref opts) = column.logical_type_options
2035                && !opts.is_empty()
2036            {
2037                let mut opts_map = serde_yaml::Mapping::new();
2038                if let Some(min_len) = opts.min_length {
2039                    opts_map.insert(
2040                        serde_yaml::Value::String("minLength".to_string()),
2041                        serde_yaml::Value::Number(serde_yaml::Number::from(min_len)),
2042                    );
2043                }
2044                if let Some(max_len) = opts.max_length {
2045                    opts_map.insert(
2046                        serde_yaml::Value::String("maxLength".to_string()),
2047                        serde_yaml::Value::Number(serde_yaml::Number::from(max_len)),
2048                    );
2049                }
2050                if let Some(ref pattern) = opts.pattern {
2051                    opts_map.insert(
2052                        serde_yaml::Value::String("pattern".to_string()),
2053                        serde_yaml::Value::String(pattern.clone()),
2054                    );
2055                }
2056                if let Some(ref format) = opts.format {
2057                    opts_map.insert(
2058                        serde_yaml::Value::String("format".to_string()),
2059                        serde_yaml::Value::String(format.clone()),
2060                    );
2061                }
2062                if let Some(ref minimum) = opts.minimum {
2063                    opts_map.insert(
2064                        serde_yaml::Value::String("minimum".to_string()),
2065                        Self::json_to_yaml_value(minimum),
2066                    );
2067                }
2068                if let Some(ref maximum) = opts.maximum {
2069                    opts_map.insert(
2070                        serde_yaml::Value::String("maximum".to_string()),
2071                        Self::json_to_yaml_value(maximum),
2072                    );
2073                }
2074                if let Some(ref exc_min) = opts.exclusive_minimum {
2075                    opts_map.insert(
2076                        serde_yaml::Value::String("exclusiveMinimum".to_string()),
2077                        Self::json_to_yaml_value(exc_min),
2078                    );
2079                }
2080                if let Some(ref exc_max) = opts.exclusive_maximum {
2081                    opts_map.insert(
2082                        serde_yaml::Value::String("exclusiveMaximum".to_string()),
2083                        Self::json_to_yaml_value(exc_max),
2084                    );
2085                }
2086                if let Some(precision) = opts.precision {
2087                    opts_map.insert(
2088                        serde_yaml::Value::String("precision".to_string()),
2089                        serde_yaml::Value::Number(serde_yaml::Number::from(precision)),
2090                    );
2091                }
2092                if let Some(scale) = opts.scale {
2093                    opts_map.insert(
2094                        serde_yaml::Value::String("scale".to_string()),
2095                        serde_yaml::Value::Number(serde_yaml::Number::from(scale)),
2096                    );
2097                }
2098                if !opts_map.is_empty() {
2099                    prop.insert(
2100                        serde_yaml::Value::String("logicalTypeOptions".to_string()),
2101                        serde_yaml::Value::Mapping(opts_map),
2102                    );
2103                }
2104            }
2105
2106            // primaryKeyPosition
2107            if let Some(pk_pos) = column.primary_key_position {
2108                prop.insert(
2109                    serde_yaml::Value::String("primaryKeyPosition".to_string()),
2110                    serde_yaml::Value::Number(serde_yaml::Number::from(pk_pos)),
2111                );
2112            }
2113
2114            // unique
2115            if column.unique {
2116                prop.insert(
2117                    serde_yaml::Value::String("unique".to_string()),
2118                    serde_yaml::Value::Bool(true),
2119                );
2120            }
2121
2122            // partitioned
2123            if column.partitioned {
2124                prop.insert(
2125                    serde_yaml::Value::String("partitioned".to_string()),
2126                    serde_yaml::Value::Bool(true),
2127                );
2128            }
2129
2130            // partitionKeyPosition
2131            if let Some(part_pos) = column.partition_key_position {
2132                prop.insert(
2133                    serde_yaml::Value::String("partitionKeyPosition".to_string()),
2134                    serde_yaml::Value::Number(serde_yaml::Number::from(part_pos)),
2135                );
2136            }
2137
2138            // classification
2139            if let Some(ref class) = column.classification {
2140                prop.insert(
2141                    serde_yaml::Value::String("classification".to_string()),
2142                    serde_yaml::Value::String(class.clone()),
2143                );
2144            }
2145
2146            // criticalDataElement
2147            if column.critical_data_element {
2148                prop.insert(
2149                    serde_yaml::Value::String("criticalDataElement".to_string()),
2150                    serde_yaml::Value::Bool(true),
2151                );
2152            }
2153
2154            // encryptedName
2155            if let Some(ref enc_name) = column.encrypted_name {
2156                prop.insert(
2157                    serde_yaml::Value::String("encryptedName".to_string()),
2158                    serde_yaml::Value::String(enc_name.clone()),
2159                );
2160            }
2161
2162            // transformSourceObjects
2163            if !column.transform_source_objects.is_empty() {
2164                let sources: Vec<serde_yaml::Value> = column
2165                    .transform_source_objects
2166                    .iter()
2167                    .map(|s| serde_yaml::Value::String(s.clone()))
2168                    .collect();
2169                prop.insert(
2170                    serde_yaml::Value::String("transformSourceObjects".to_string()),
2171                    serde_yaml::Value::Sequence(sources),
2172                );
2173            }
2174
2175            // transformLogic
2176            if let Some(ref logic) = column.transform_logic {
2177                prop.insert(
2178                    serde_yaml::Value::String("transformLogic".to_string()),
2179                    serde_yaml::Value::String(logic.clone()),
2180                );
2181            }
2182
2183            // transformDescription
2184            if let Some(ref desc) = column.transform_description {
2185                prop.insert(
2186                    serde_yaml::Value::String("transformDescription".to_string()),
2187                    serde_yaml::Value::String(desc.clone()),
2188                );
2189            }
2190
2191            // examples
2192            if !column.examples.is_empty() {
2193                let examples: Vec<serde_yaml::Value> = column
2194                    .examples
2195                    .iter()
2196                    .map(Self::json_to_yaml_value)
2197                    .collect();
2198                prop.insert(
2199                    serde_yaml::Value::String("examples".to_string()),
2200                    serde_yaml::Value::Sequence(examples),
2201                );
2202            }
2203
2204            // authoritativeDefinitions
2205            if !column.authoritative_definitions.is_empty() {
2206                let defs: Vec<serde_yaml::Value> = column
2207                    .authoritative_definitions
2208                    .iter()
2209                    .map(|d| {
2210                        let mut def_map = serde_yaml::Mapping::new();
2211                        def_map.insert(
2212                            serde_yaml::Value::String("type".to_string()),
2213                            serde_yaml::Value::String(d.definition_type.clone()),
2214                        );
2215                        def_map.insert(
2216                            serde_yaml::Value::String("url".to_string()),
2217                            serde_yaml::Value::String(d.url.clone()),
2218                        );
2219                        serde_yaml::Value::Mapping(def_map)
2220                    })
2221                    .collect();
2222                prop.insert(
2223                    serde_yaml::Value::String("authoritativeDefinitions".to_string()),
2224                    serde_yaml::Value::Sequence(defs),
2225                );
2226            }
2227
2228            // tags
2229            if !column.tags.is_empty() {
2230                let tags: Vec<serde_yaml::Value> = column
2231                    .tags
2232                    .iter()
2233                    .map(|t| serde_yaml::Value::String(t.clone()))
2234                    .collect();
2235                prop.insert(
2236                    serde_yaml::Value::String("tags".to_string()),
2237                    serde_yaml::Value::Sequence(tags),
2238                );
2239            }
2240
2241            // customProperties
2242            if !column.custom_properties.is_empty() {
2243                let custom_props: Vec<serde_yaml::Value> = column
2244                    .custom_properties
2245                    .iter()
2246                    .map(|(key, value)| {
2247                        let mut prop_map = serde_yaml::Mapping::new();
2248                        prop_map.insert(
2249                            serde_yaml::Value::String("property".to_string()),
2250                            serde_yaml::Value::String(key.clone()),
2251                        );
2252                        prop_map.insert(
2253                            serde_yaml::Value::String("value".to_string()),
2254                            Self::json_to_yaml_value(value),
2255                        );
2256                        serde_yaml::Value::Mapping(prop_map)
2257                    })
2258                    .collect();
2259                prop.insert(
2260                    serde_yaml::Value::String("customProperties".to_string()),
2261                    serde_yaml::Value::Sequence(custom_props),
2262                );
2263            }
2264
2265            // Export foreign key
2266            if let Some(ref fk) = column.foreign_key {
2267                let mut fk_map = serde_yaml::Mapping::new();
2268                fk_map.insert(
2269                    serde_yaml::Value::String("table".to_string()),
2270                    serde_yaml::Value::String(fk.table_id.clone()),
2271                );
2272                fk_map.insert(
2273                    serde_yaml::Value::String("column".to_string()),
2274                    serde_yaml::Value::String(fk.column_name.clone()),
2275                );
2276                prop.insert(
2277                    serde_yaml::Value::String("foreignKey".to_string()),
2278                    serde_yaml::Value::Mapping(fk_map),
2279                );
2280            }
2281
2282            // Add 'id' and 'name' fields to property object (required in ODCS v3.1.0)
2283            // Generate ID from name (convert to snake_case)
2284            let id = column
2285                .name
2286                .chars()
2287                .map(|c| {
2288                    if c.is_alphanumeric() {
2289                        c.to_lowercase().to_string()
2290                    } else {
2291                        "_".to_string()
2292                    }
2293                })
2294                .collect::<String>()
2295                .replace("__", "_");
2296
2297            prop.insert(
2298                serde_yaml::Value::String("id".to_string()),
2299                serde_yaml::Value::String(format!("{}_obj", id)),
2300            );
2301            prop.insert(
2302                serde_yaml::Value::String("name".to_string()),
2303                serde_yaml::Value::String(column.name.clone()),
2304            );
2305
2306            properties.push(serde_yaml::Value::Mapping(prop));
2307        }
2308
2309        schema_obj.insert(
2310            serde_yaml::Value::String("properties".to_string()),
2311            serde_yaml::Value::Sequence(properties),
2312        );
2313
2314        schema_array.push(serde_yaml::Value::Mapping(schema_obj));
2315        yaml.insert(
2316            serde_yaml::Value::String("schema".to_string()),
2317            serde_yaml::Value::Sequence(schema_array),
2318        );
2319
2320        // Table-level quality rules
2321        if !table.quality.is_empty() {
2322            let quality_array: Vec<serde_yaml::Value> = table
2323                .quality
2324                .iter()
2325                .map(|rule| {
2326                    let mut rule_map = serde_yaml::Mapping::new();
2327                    for (k, v) in rule {
2328                        rule_map.insert(
2329                            serde_yaml::Value::String(k.clone()),
2330                            Self::json_to_yaml_value(v),
2331                        );
2332                    }
2333                    serde_yaml::Value::Mapping(rule_map)
2334                })
2335                .collect();
2336            yaml.insert(
2337                serde_yaml::Value::String("quality".to_string()),
2338                serde_yaml::Value::Sequence(quality_array),
2339            );
2340        }
2341
2342        // Custom Properties from metadata (excluding already exported fields)
2343        let excluded_keys = [
2344            "id",
2345            "version",
2346            "status",
2347            "domain",
2348            "dataProduct",
2349            "tenant",
2350            "description",
2351            "team",
2352            "roles",
2353            "pricing",
2354            "terms",
2355            "servers",
2356            "servicelevels",
2357            "links",
2358            "apiVersion",
2359            "kind",
2360            "info",
2361            "dataContractSpecification",
2362        ];
2363
2364        let mut custom_props = Vec::new();
2365        for (key, value) in &table.odcl_metadata {
2366            if !excluded_keys.contains(&key.as_str()) && !value.is_null() {
2367                let mut prop = serde_yaml::Mapping::new();
2368                prop.insert(
2369                    serde_yaml::Value::String("property".to_string()),
2370                    serde_yaml::Value::String(key.clone()),
2371                );
2372                prop.insert(
2373                    serde_yaml::Value::String("value".to_string()),
2374                    Self::json_to_yaml_value(value),
2375                );
2376                custom_props.push(serde_yaml::Value::Mapping(prop));
2377            }
2378        }
2379
2380        // Add database type as custom property if present
2381        if let Some(ref db_type) = table.database_type {
2382            let mut prop = serde_yaml::Mapping::new();
2383            prop.insert(
2384                serde_yaml::Value::String("property".to_string()),
2385                serde_yaml::Value::String("databaseType".to_string()),
2386            );
2387            prop.insert(
2388                serde_yaml::Value::String("value".to_string()),
2389                serde_yaml::Value::String(format!("{:?}", db_type)),
2390            );
2391            custom_props.push(serde_yaml::Value::Mapping(prop));
2392        }
2393
2394        // Add medallion layers as custom property if present
2395        if !table.medallion_layers.is_empty() {
2396            let layers: Vec<serde_yaml::Value> = table
2397                .medallion_layers
2398                .iter()
2399                .map(|l| serde_yaml::Value::String(format!("{:?}", l)))
2400                .collect();
2401            let mut prop = serde_yaml::Mapping::new();
2402            prop.insert(
2403                serde_yaml::Value::String("property".to_string()),
2404                serde_yaml::Value::String("medallionLayers".to_string()),
2405            );
2406            prop.insert(
2407                serde_yaml::Value::String("value".to_string()),
2408                serde_yaml::Value::Sequence(layers),
2409            );
2410            custom_props.push(serde_yaml::Value::Mapping(prop));
2411        }
2412
2413        // Add SCD pattern as custom property if present
2414        if let Some(ref scd_pattern) = table.scd_pattern {
2415            let mut prop = serde_yaml::Mapping::new();
2416            prop.insert(
2417                serde_yaml::Value::String("property".to_string()),
2418                serde_yaml::Value::String("scdPattern".to_string()),
2419            );
2420            prop.insert(
2421                serde_yaml::Value::String("value".to_string()),
2422                serde_yaml::Value::String(format!("{:?}", scd_pattern)),
2423            );
2424            custom_props.push(serde_yaml::Value::Mapping(prop));
2425        }
2426
2427        // Add Data Vault classification as custom property if present
2428        if let Some(ref dv_class) = table.data_vault_classification {
2429            let mut prop = serde_yaml::Mapping::new();
2430            prop.insert(
2431                serde_yaml::Value::String("property".to_string()),
2432                serde_yaml::Value::String("dataVaultClassification".to_string()),
2433            );
2434            prop.insert(
2435                serde_yaml::Value::String("value".to_string()),
2436                serde_yaml::Value::String(format!("{:?}", dv_class)),
2437            );
2438            custom_props.push(serde_yaml::Value::Mapping(prop));
2439        }
2440
2441        // Add catalog/schema names as custom properties if present
2442        if let Some(ref catalog) = table.catalog_name {
2443            let mut prop = serde_yaml::Mapping::new();
2444            prop.insert(
2445                serde_yaml::Value::String("property".to_string()),
2446                serde_yaml::Value::String("catalogName".to_string()),
2447            );
2448            prop.insert(
2449                serde_yaml::Value::String("value".to_string()),
2450                serde_yaml::Value::String(catalog.clone()),
2451            );
2452            custom_props.push(serde_yaml::Value::Mapping(prop));
2453        }
2454
2455        if let Some(ref schema) = table.schema_name {
2456            let mut prop = serde_yaml::Mapping::new();
2457            prop.insert(
2458                serde_yaml::Value::String("property".to_string()),
2459                serde_yaml::Value::String("schemaName".to_string()),
2460            );
2461            prop.insert(
2462                serde_yaml::Value::String("value".to_string()),
2463                serde_yaml::Value::String(schema.clone()),
2464            );
2465            custom_props.push(serde_yaml::Value::Mapping(prop));
2466        }
2467
2468        if !custom_props.is_empty() {
2469            yaml.insert(
2470                serde_yaml::Value::String("customProperties".to_string()),
2471                serde_yaml::Value::Sequence(custom_props),
2472            );
2473        }
2474
2475        // Contract created timestamp
2476        yaml.insert(
2477            serde_yaml::Value::String("contractCreatedTs".to_string()),
2478            serde_yaml::Value::String(table.created_at.to_rfc3339()),
2479        );
2480
2481        serde_yaml::to_string(&yaml).unwrap_or_default()
2482    }
2483
2484    /// Export tables to ODCS v3.1.0 YAML format (SDK interface).
2485    pub fn export(
2486        &self,
2487        tables: &[Table],
2488        _format: &str,
2489    ) -> Result<HashMap<String, ExportResult>, ExportError> {
2490        let mut exports = HashMap::new();
2491        for table in tables {
2492            // All exports use ODCS v3.1.0 format
2493            let yaml = Self::export_odcs_v3_1_0_format(table);
2494
2495            // Validate exported YAML against ODCS schema (if feature enabled)
2496            #[cfg(feature = "schema-validation")]
2497            {
2498                use crate::validation::schema::validate_odcs_internal;
2499                validate_odcs_internal(&yaml).map_err(|e| {
2500                    ExportError::ValidationError(format!("ODCS validation failed: {}", e))
2501                })?;
2502            }
2503
2504            exports.insert(
2505                table.name.clone(),
2506                ExportResult {
2507                    content: yaml,
2508                    format: "odcs_v3_1_0".to_string(),
2509                },
2510            );
2511        }
2512        Ok(exports)
2513    }
2514
2515    /// Export a data model to ODCS v3.1.0 YAML format (legacy method for compatibility).
2516    pub fn export_model(
2517        model: &DataModel,
2518        table_ids: Option<&[uuid::Uuid]>,
2519        _format: &str,
2520    ) -> HashMap<String, String> {
2521        let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
2522            model
2523                .tables
2524                .iter()
2525                .filter(|t| ids.contains(&t.id))
2526                .collect()
2527        } else {
2528            model.tables.iter().collect()
2529        };
2530
2531        let mut exports = HashMap::new();
2532        for table in tables_to_export {
2533            // All exports use ODCS v3.1.0 format
2534            let yaml = Self::export_odcs_v3_1_0_format(table);
2535            exports.insert(table.name.clone(), yaml);
2536        }
2537
2538        exports
2539    }
2540}
2541
2542#[cfg(test)]
2543mod tests {
2544    use super::*;
2545    use crate::models::{Column, Tag};
2546
2547    #[test]
2548    fn test_export_odcs_v3_1_0_basic() {
2549        let table = Table {
2550            id: Table::generate_id("test_table", None, None, None),
2551            name: "test_table".to_string(),
2552            columns: vec![Column {
2553                name: "id".to_string(),
2554                data_type: "BIGINT".to_string(),
2555                nullable: false,
2556                primary_key: true,
2557                description: "Primary key".to_string(),
2558                ..Default::default()
2559            }],
2560            database_type: None,
2561            catalog_name: None,
2562            schema_name: None,
2563            medallion_layers: Vec::new(),
2564            scd_pattern: None,
2565            data_vault_classification: None,
2566            modeling_level: None,
2567            tags: vec![Tag::Simple("test".to_string())],
2568            odcl_metadata: HashMap::new(),
2569            owner: None,
2570            sla: None,
2571            contact_details: None,
2572            infrastructure_type: None,
2573            notes: None,
2574            position: None,
2575            yaml_file_path: None,
2576            drawio_cell_id: None,
2577            quality: Vec::new(),
2578            errors: Vec::new(),
2579            created_at: chrono::Utc::now(),
2580            updated_at: chrono::Utc::now(),
2581        };
2582
2583        let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
2584
2585        assert!(yaml.contains("apiVersion: v3.1.0"));
2586        assert!(yaml.contains("kind: DataContract"));
2587        assert!(yaml.contains("name: test_table"));
2588        assert!(yaml.contains("tags:"));
2589        assert!(yaml.contains("- test"));
2590    }
2591}