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