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