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                    nested_props_map.insert(
943                        serde_yaml::Value::String(child_name.clone()),
944                        serde_yaml::Value::Mapping(child_prop),
945                    );
946                } else if !child_cols.is_empty() {
947                    // No exact match found, but we have columns for this child_name
948                    // Use the first column that matches child_name exactly (should be the direct child)
949                    if let Some(first_col) = child_cols.iter().find(|col| {
950                        let rel_name = if col.name.starts_with(&parent_prefix_array) {
951                            col.name.strip_prefix(&parent_prefix_array)
952                        } else if col.name.starts_with(&parent_prefix_dot) {
953                            col.name.strip_prefix(&parent_prefix_dot)
954                        } else {
955                            None
956                        };
957                        rel_name.map(|rel| rel == child_name).unwrap_or(false)
958                    }) {
959                        let mut child_prop = serde_yaml::Mapping::new();
960
961                        // Add the name field (required in ODCS v3.1.0)
962                        child_prop.insert(
963                            serde_yaml::Value::String("name".to_string()),
964                            serde_yaml::Value::String(child_name.clone()),
965                        );
966
967                        // Check if this child has nested children
968                        let child_has_nested = child_cols.iter().any(|col| {
969                            let rel_name = if col.name.starts_with(&parent_prefix_array) {
970                                col.name.strip_prefix(&parent_prefix_array)
971                            } else if col.name.starts_with(&parent_prefix_dot) {
972                                col.name.strip_prefix(&parent_prefix_dot)
973                            } else {
974                                None
975                            };
976                            rel_name
977                                .map(|rel| {
978                                    rel.starts_with(&format!("{}.", child_name))
979                                        && rel != child_name
980                                })
981                                .unwrap_or(false)
982                        });
983
984                        // Try to build nested properties if there are nested children
985                        let nested_props_array = if child_has_nested {
986                            build_nested_properties(
987                                &first_col.name,
988                                _table_name,
989                                all_columns,
990                                json_to_yaml_fn,
991                                map_data_type_fn,
992                            )
993                        } else {
994                            None
995                        };
996
997                        let data_type_upper = first_col.data_type.to_uppercase();
998                        let is_array_object = data_type_upper.starts_with("ARRAY<")
999                            && (data_type_upper.contains("OBJECT")
1000                                || data_type_upper.contains("STRUCT"));
1001                        let is_struct_or_object = data_type_upper == "STRUCT"
1002                            || data_type_upper == "OBJECT"
1003                            || data_type_upper.starts_with("STRUCT<");
1004
1005                        if is_array_object && (child_has_nested || nested_props_array.is_some()) {
1006                            child_prop.insert(
1007                                serde_yaml::Value::String("logicalType".to_string()),
1008                                serde_yaml::Value::String("array".to_string()),
1009                            );
1010                            child_prop.insert(
1011                                serde_yaml::Value::String("physicalType".to_string()),
1012                                serde_yaml::Value::String(get_physical_type(first_col)),
1013                            );
1014
1015                            let mut items = serde_yaml::Mapping::new();
1016                            items.insert(
1017                                serde_yaml::Value::String("logicalType".to_string()),
1018                                serde_yaml::Value::String("object".to_string()),
1019                            );
1020
1021                            if let Some(nested_props) = nested_props_array {
1022                                items.insert(
1023                                    serde_yaml::Value::String("properties".to_string()),
1024                                    serde_yaml::Value::Sequence(nested_props),
1025                                );
1026                            }
1027
1028                            child_prop.insert(
1029                                serde_yaml::Value::String("items".to_string()),
1030                                serde_yaml::Value::Mapping(items),
1031                            );
1032                        } else if is_struct_or_object
1033                            || child_has_nested
1034                            || nested_props_array.is_some()
1035                        {
1036                            child_prop.insert(
1037                                serde_yaml::Value::String("logicalType".to_string()),
1038                                serde_yaml::Value::String("object".to_string()),
1039                            );
1040                            child_prop.insert(
1041                                serde_yaml::Value::String("physicalType".to_string()),
1042                                serde_yaml::Value::String(get_physical_type(first_col)),
1043                            );
1044
1045                            if let Some(nested_props) = nested_props_array {
1046                                child_prop.insert(
1047                                    serde_yaml::Value::String("properties".to_string()),
1048                                    serde_yaml::Value::Sequence(nested_props),
1049                                );
1050                            }
1051                        } else {
1052                            let (logical_type, _) = map_data_type_fn(&first_col.data_type);
1053                            child_prop.insert(
1054                                serde_yaml::Value::String("logicalType".to_string()),
1055                                serde_yaml::Value::String(logical_type),
1056                            );
1057                            child_prop.insert(
1058                                serde_yaml::Value::String("physicalType".to_string()),
1059                                serde_yaml::Value::String(get_physical_type(first_col)),
1060                            );
1061                        }
1062
1063                        if !first_col.nullable {
1064                            child_prop.insert(
1065                                serde_yaml::Value::String("required".to_string()),
1066                                serde_yaml::Value::Bool(true),
1067                            );
1068                        }
1069
1070                        if !first_col.description.is_empty() {
1071                            child_prop.insert(
1072                                serde_yaml::Value::String("description".to_string()),
1073                                serde_yaml::Value::String(first_col.description.clone()),
1074                            );
1075                        }
1076
1077                        nested_props_map.insert(
1078                            serde_yaml::Value::String(child_name.clone()),
1079                            serde_yaml::Value::Mapping(child_prop),
1080                        );
1081                    }
1082                } else {
1083                    // No columns found for this child - skip
1084                    continue;
1085                }
1086            }
1087
1088            if nested_props_map.is_empty() {
1089                None
1090            } else {
1091                // Convert Mapping to Array format (ODCS v3.1.0)
1092                Some(mapping_to_properties_array(nested_props_map))
1093            }
1094        }
1095
1096        for column in &table.columns {
1097            // Skip nested columns (they're handled as part of parent columns)
1098            if column.name.contains('.') {
1099                continue;
1100            }
1101
1102            let mut prop = serde_yaml::Mapping::new();
1103
1104            // Check if this column has nested columns
1105            // Handle both dot notation (parent.field) and array notation (parent.[].field)
1106            // Also handle deeply nested structures like parent.[].nested.[].field
1107            let column_prefix_dot = format!("{}.", column.name);
1108            let column_prefix_array = format!("{}.[]", column.name);
1109            let has_nested = table.columns.iter().any(|col| {
1110                (col.name.starts_with(&column_prefix_dot)
1111                    || col.name.starts_with(&column_prefix_array))
1112                    && col.name != column.name
1113            });
1114
1115            // Determine the type - handle ARRAY, STRUCT, OBJECT, etc.
1116            let data_type_upper = column.data_type.to_uppercase();
1117
1118            // Check if this is ARRAY<STRUCT> - can be detected in two ways:
1119            // 1. data_type == "ARRAY" with full type in description after ||
1120            // 2. data_type == "ARRAY" or "ARRAY<OBJECT>" with nested columns using .[] notation
1121            let is_array_struct_from_desc =
1122                data_type_upper == "ARRAY" && column.description.contains("|| ARRAY<STRUCT<");
1123            let is_array_struct_from_nested = (data_type_upper == "ARRAY"
1124                || data_type_upper == "ARRAY<OBJECT>")
1125                && has_nested
1126                && table
1127                    .columns
1128                    .iter()
1129                    .any(|col| col.name.starts_with(&format!("{}.[]", column.name)));
1130
1131            // Also check if data_type is ARRAY<STRING> but description contains ARRAY<STRUCT< (from SQL export)
1132            let is_array_struct_from_string_type = data_type_upper == "ARRAY<STRING>"
1133                && column.description.contains("|| ARRAY<STRUCT<");
1134
1135            let is_array_struct = is_array_struct_from_desc
1136                || is_array_struct_from_nested
1137                || is_array_struct_from_string_type;
1138
1139            // Extract the full ARRAY<STRUCT<...>> type from description if present
1140            let full_array_struct_type =
1141                if is_array_struct_from_desc || is_array_struct_from_string_type {
1142                    column
1143                        .description
1144                        .split("|| ")
1145                        .nth(1)
1146                        .map(|s| s.trim().to_string())
1147                } else if is_array_struct_from_nested {
1148                    // Reconstruct the STRUCT type from nested columns
1149                    // We'll use build_nested_properties to get the structure, then reconstruct the type string
1150                    None // Will be handled by parsing nested columns
1151                } else {
1152                    None
1153                };
1154
1155            let is_array_object = data_type_upper.starts_with("ARRAY<")
1156                && (data_type_upper.contains("OBJECT") || data_type_upper.contains("STRUCT"));
1157            let is_struct_or_object = data_type_upper == "STRUCT"
1158                || data_type_upper == "OBJECT"
1159                || data_type_upper.starts_with("STRUCT<");
1160
1161            // Handle ARRAY<STRUCT> types first - parse from description field or nested columns
1162            if is_array_struct {
1163                let struct_props = if let Some(ref full_type) = full_array_struct_type {
1164                    // Parse the STRUCT definition from the full type string (from SQL import)
1165                    Self::parse_struct_properties_from_data_type(
1166                        &column.name,
1167                        full_type,
1168                        &Self::map_data_type_to_logical_type,
1169                    )
1170                } else if is_array_struct_from_nested {
1171                    // Build properties from nested columns (from ODCS import)
1172                    build_nested_properties(
1173                        &column.name,
1174                        &table.name,
1175                        &table.columns,
1176                        &Self::json_to_yaml_value,
1177                        &Self::map_data_type_to_logical_type,
1178                    )
1179                } else {
1180                    None
1181                };
1182
1183                if let Some(props) = struct_props {
1184                    // Create array of objects structure
1185                    prop.insert(
1186                        serde_yaml::Value::String("logicalType".to_string()),
1187                        serde_yaml::Value::String("array".to_string()),
1188                    );
1189                    prop.insert(
1190                        serde_yaml::Value::String("physicalType".to_string()),
1191                        serde_yaml::Value::String("ARRAY".to_string()),
1192                    );
1193
1194                    let mut items = serde_yaml::Mapping::new();
1195                    items.insert(
1196                        serde_yaml::Value::String("logicalType".to_string()),
1197                        serde_yaml::Value::String("object".to_string()),
1198                    );
1199                    items.insert(
1200                        serde_yaml::Value::String("properties".to_string()),
1201                        serde_yaml::Value::Sequence(props),
1202                    );
1203
1204                    prop.insert(
1205                        serde_yaml::Value::String("items".to_string()),
1206                        serde_yaml::Value::Mapping(items),
1207                    );
1208
1209                    // Extract base description (before ||) if present
1210                    if is_array_struct_from_desc {
1211                        let base_description =
1212                            column.description.split("||").next().unwrap_or("").trim();
1213                        if !base_description.is_empty() {
1214                            prop.insert(
1215                                serde_yaml::Value::String("description".to_string()),
1216                                serde_yaml::Value::String(base_description.to_string()),
1217                            );
1218                        }
1219                    } else if !column.description.is_empty() {
1220                        // Use full description if not from SQL import
1221                        prop.insert(
1222                            serde_yaml::Value::String("description".to_string()),
1223                            serde_yaml::Value::String(column.description.clone()),
1224                        );
1225                    }
1226                } else {
1227                    // Fallback: if parsing fails, just use simple array type
1228                    prop.insert(
1229                        serde_yaml::Value::String("logicalType".to_string()),
1230                        serde_yaml::Value::String("array".to_string()),
1231                    );
1232                    prop.insert(
1233                        serde_yaml::Value::String("physicalType".to_string()),
1234                        serde_yaml::Value::String("ARRAY".to_string()),
1235                    );
1236                }
1237            }
1238            // Always check for nested properties if nested columns exist
1239            // Also handle STRUCT columns even if nested columns weren't created by SQL parser
1240            else if has_nested {
1241                // Try to build nested properties first
1242                let nested_props = build_nested_properties(
1243                    &column.name,
1244                    &table.name,
1245                    &table.columns,
1246                    &Self::json_to_yaml_value,
1247                    &Self::map_data_type_to_logical_type,
1248                );
1249
1250                if is_array_object {
1251                    // ARRAY<OBJECT> with nested fields
1252                    prop.insert(
1253                        serde_yaml::Value::String("logicalType".to_string()),
1254                        serde_yaml::Value::String("array".to_string()),
1255                    );
1256                    prop.insert(
1257                        serde_yaml::Value::String("physicalType".to_string()),
1258                        serde_yaml::Value::String(get_physical_type(column)),
1259                    );
1260
1261                    let mut items = serde_yaml::Mapping::new();
1262                    items.insert(
1263                        serde_yaml::Value::String("logicalType".to_string()),
1264                        serde_yaml::Value::String("object".to_string()),
1265                    );
1266
1267                    // Add nested properties if they exist (already in array format)
1268                    if let Some(nested_props_array) = nested_props {
1269                        items.insert(
1270                            serde_yaml::Value::String("properties".to_string()),
1271                            serde_yaml::Value::Sequence(nested_props_array),
1272                        );
1273                    }
1274
1275                    prop.insert(
1276                        serde_yaml::Value::String("items".to_string()),
1277                        serde_yaml::Value::Mapping(items),
1278                    );
1279                } else if is_struct_or_object || nested_props.is_some() {
1280                    // OBJECT/STRUCT with nested fields, or any column with nested columns
1281                    prop.insert(
1282                        serde_yaml::Value::String("logicalType".to_string()),
1283                        serde_yaml::Value::String("object".to_string()),
1284                    );
1285                    prop.insert(
1286                        serde_yaml::Value::String("physicalType".to_string()),
1287                        serde_yaml::Value::String(get_physical_type(column)),
1288                    );
1289
1290                    // Add nested properties if they exist (already in array format)
1291                    if let Some(nested_props_array) = nested_props {
1292                        prop.insert(
1293                            serde_yaml::Value::String("properties".to_string()),
1294                            serde_yaml::Value::Sequence(nested_props_array),
1295                        );
1296                    }
1297                } else {
1298                    // Has nested columns but couldn't build structure - use simple type
1299                    // But if it's ARRAY<OBJECT> with nested columns, still export as ARRAY with items
1300                    if is_array_object && has_nested {
1301                        // Even if build_nested_properties failed, try to create a simple array structure
1302                        prop.insert(
1303                            serde_yaml::Value::String("logicalType".to_string()),
1304                            serde_yaml::Value::String("array".to_string()),
1305                        );
1306                        prop.insert(
1307                            serde_yaml::Value::String("physicalType".to_string()),
1308                            serde_yaml::Value::String("ARRAY".to_string()),
1309                        );
1310
1311                        let mut items = serde_yaml::Mapping::new();
1312                        items.insert(
1313                            serde_yaml::Value::String("logicalType".to_string()),
1314                            serde_yaml::Value::String("object".to_string()),
1315                        );
1316
1317                        // Try build_nested_properties one more time - it might work now
1318                        if let Some(nested_props_array) = build_nested_properties(
1319                            &column.name,
1320                            &table.name,
1321                            &table.columns,
1322                            &Self::json_to_yaml_value,
1323                            &Self::map_data_type_to_logical_type,
1324                        ) {
1325                            items.insert(
1326                                serde_yaml::Value::String("properties".to_string()),
1327                                serde_yaml::Value::Sequence(nested_props_array),
1328                            );
1329                        }
1330
1331                        prop.insert(
1332                            serde_yaml::Value::String("items".to_string()),
1333                            serde_yaml::Value::Mapping(items),
1334                        );
1335                    } else {
1336                        let (logical_type, _) =
1337                            Self::map_data_type_to_logical_type(&column.data_type);
1338                        prop.insert(
1339                            serde_yaml::Value::String("logicalType".to_string()),
1340                            serde_yaml::Value::String(logical_type),
1341                        );
1342                        prop.insert(
1343                            serde_yaml::Value::String("physicalType".to_string()),
1344                            serde_yaml::Value::String(get_physical_type(column)),
1345                        );
1346                    }
1347                }
1348            } else if is_struct_or_object {
1349                // STRUCT/OBJECT type but no nested columns (e.g., from SQL parser that didn't create nested columns)
1350                // Try to parse STRUCT definition from data_type to create nested properties
1351                let parsed_props = Self::parse_struct_properties_from_data_type(
1352                    &column.name,
1353                    &column.data_type,
1354                    &Self::map_data_type_to_logical_type,
1355                );
1356
1357                prop.insert(
1358                    serde_yaml::Value::String("logicalType".to_string()),
1359                    serde_yaml::Value::String("object".to_string()),
1360                );
1361                prop.insert(
1362                    serde_yaml::Value::String("physicalType".to_string()),
1363                    serde_yaml::Value::String(get_physical_type(column)),
1364                );
1365
1366                // Add parsed nested properties if available
1367                if let Some(nested_props_array) = parsed_props {
1368                    prop.insert(
1369                        serde_yaml::Value::String("properties".to_string()),
1370                        serde_yaml::Value::Sequence(nested_props_array),
1371                    );
1372                }
1373            } else if prop.is_empty() {
1374                // No nested columns and prop is empty - use simple type
1375                let (logical_type, _) = Self::map_data_type_to_logical_type(&column.data_type);
1376                prop.insert(
1377                    serde_yaml::Value::String("logicalType".to_string()),
1378                    serde_yaml::Value::String(logical_type),
1379                );
1380                prop.insert(
1381                    serde_yaml::Value::String("physicalType".to_string()),
1382                    serde_yaml::Value::String(get_physical_type(column)),
1383                );
1384            }
1385            // If prop is not empty (was set by is_array_struct or has_nested blocks), use it as-is
1386
1387            if !column.nullable {
1388                prop.insert(
1389                    serde_yaml::Value::String("required".to_string()),
1390                    serde_yaml::Value::Bool(true),
1391                );
1392            }
1393
1394            if column.primary_key {
1395                prop.insert(
1396                    serde_yaml::Value::String("primaryKey".to_string()),
1397                    serde_yaml::Value::Bool(true),
1398                );
1399            }
1400
1401            if column.secondary_key {
1402                prop.insert(
1403                    serde_yaml::Value::String("businessKey".to_string()),
1404                    serde_yaml::Value::Bool(true),
1405                );
1406            }
1407
1408            if !column.description.is_empty() {
1409                prop.insert(
1410                    serde_yaml::Value::String("description".to_string()),
1411                    serde_yaml::Value::String(column.description.clone()),
1412                );
1413            }
1414
1415            // Export column-level quality rules
1416            if !column.quality.is_empty() {
1417                let quality_array: Vec<serde_yaml::Value> = column
1418                    .quality
1419                    .iter()
1420                    .map(|rule| {
1421                        let mut rule_map = serde_yaml::Mapping::new();
1422                        for (k, v) in rule {
1423                            rule_map.insert(
1424                                serde_yaml::Value::String(k.clone()),
1425                                Self::json_to_yaml_value(v),
1426                            );
1427                        }
1428                        serde_yaml::Value::Mapping(rule_map)
1429                    })
1430                    .collect();
1431                prop.insert(
1432                    serde_yaml::Value::String("quality".to_string()),
1433                    serde_yaml::Value::Sequence(quality_array),
1434                );
1435            }
1436
1437            // Export relationships array (ODCS v3.1.0 format)
1438            if !column.relationships.is_empty() {
1439                let rels_yaml: Vec<serde_yaml::Value> = column
1440                    .relationships
1441                    .iter()
1442                    .map(|rel| {
1443                        let mut rel_map = serde_yaml::Mapping::new();
1444                        rel_map.insert(
1445                            serde_yaml::Value::String("type".to_string()),
1446                            serde_yaml::Value::String(rel.relationship_type.clone()),
1447                        );
1448                        rel_map.insert(
1449                            serde_yaml::Value::String("to".to_string()),
1450                            serde_yaml::Value::String(rel.to.clone()),
1451                        );
1452                        serde_yaml::Value::Mapping(rel_map)
1453                    })
1454                    .collect();
1455                prop.insert(
1456                    serde_yaml::Value::String("relationships".to_string()),
1457                    serde_yaml::Value::Sequence(rels_yaml),
1458                );
1459            }
1460
1461            // Convert enum values to ODCS quality rules
1462            // ODCS v3.1.0 doesn't support 'enum' field in properties - use quality rules instead
1463            // Use SQL type with IN clause to validate enum values
1464            if !column.enum_values.is_empty() {
1465                // Check if there's already a quality array, if not create one
1466                let quality = prop
1467                    .entry(serde_yaml::Value::String("quality".to_string()))
1468                    .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
1469
1470                if let serde_yaml::Value::Sequence(quality_rules) = quality {
1471                    // Create a SQL quality rule for enum values
1472                    // Use SQL type with IN clause to validate enum values
1473                    let mut enum_rule = serde_yaml::Mapping::new();
1474                    enum_rule.insert(
1475                        serde_yaml::Value::String("type".to_string()),
1476                        serde_yaml::Value::String("sql".to_string()),
1477                    );
1478
1479                    // Build SQL query with IN clause for enum values
1480                    let enum_list: String = column
1481                        .enum_values
1482                        .iter()
1483                        .map(|e| format!("'{}'", e.replace('\'', "''"))) // Escape single quotes
1484                        .collect::<Vec<_>>()
1485                        .join(", ");
1486                    let query = format!(
1487                        "SELECT COUNT(*) FROM ${{table}} WHERE ${{column}} NOT IN ({})",
1488                        enum_list
1489                    );
1490
1491                    enum_rule.insert(
1492                        serde_yaml::Value::String("query".to_string()),
1493                        serde_yaml::Value::String(query),
1494                    );
1495
1496                    enum_rule.insert(
1497                        serde_yaml::Value::String("mustBe".to_string()),
1498                        serde_yaml::Value::Number(serde_yaml::Number::from(0)),
1499                    );
1500
1501                    enum_rule.insert(
1502                        serde_yaml::Value::String("description".to_string()),
1503                        serde_yaml::Value::String(format!(
1504                            "Value must be one of: {}",
1505                            column.enum_values.join(", ")
1506                        )),
1507                    );
1508
1509                    quality_rules.push(serde_yaml::Value::Mapping(enum_rule));
1510                }
1511            }
1512
1513            // Export constraints
1514            if !column.constraints.is_empty() {
1515                let constraints_yaml: Vec<serde_yaml::Value> = column
1516                    .constraints
1517                    .iter()
1518                    .map(|c| serde_yaml::Value::String(c.clone()))
1519                    .collect();
1520                prop.insert(
1521                    serde_yaml::Value::String("constraints".to_string()),
1522                    serde_yaml::Value::Sequence(constraints_yaml),
1523                );
1524            }
1525
1526            // === Additional ODCS v3.1.0 Column Fields ===
1527
1528            // businessName
1529            if let Some(ref biz_name) = column.business_name {
1530                prop.insert(
1531                    serde_yaml::Value::String("businessName".to_string()),
1532                    serde_yaml::Value::String(biz_name.clone()),
1533                );
1534            }
1535
1536            // physicalName
1537            if let Some(ref phys_name) = column.physical_name {
1538                prop.insert(
1539                    serde_yaml::Value::String("physicalName".to_string()),
1540                    serde_yaml::Value::String(phys_name.clone()),
1541                );
1542            }
1543
1544            // logicalTypeOptions
1545            if let Some(ref opts) = column.logical_type_options
1546                && !opts.is_empty()
1547            {
1548                let mut opts_map = serde_yaml::Mapping::new();
1549                if let Some(min_len) = opts.min_length {
1550                    opts_map.insert(
1551                        serde_yaml::Value::String("minLength".to_string()),
1552                        serde_yaml::Value::Number(serde_yaml::Number::from(min_len)),
1553                    );
1554                }
1555                if let Some(max_len) = opts.max_length {
1556                    opts_map.insert(
1557                        serde_yaml::Value::String("maxLength".to_string()),
1558                        serde_yaml::Value::Number(serde_yaml::Number::from(max_len)),
1559                    );
1560                }
1561                if let Some(ref pattern) = opts.pattern {
1562                    opts_map.insert(
1563                        serde_yaml::Value::String("pattern".to_string()),
1564                        serde_yaml::Value::String(pattern.clone()),
1565                    );
1566                }
1567                if let Some(ref format) = opts.format {
1568                    opts_map.insert(
1569                        serde_yaml::Value::String("format".to_string()),
1570                        serde_yaml::Value::String(format.clone()),
1571                    );
1572                }
1573                if let Some(ref minimum) = opts.minimum {
1574                    opts_map.insert(
1575                        serde_yaml::Value::String("minimum".to_string()),
1576                        Self::json_to_yaml_value(minimum),
1577                    );
1578                }
1579                if let Some(ref maximum) = opts.maximum {
1580                    opts_map.insert(
1581                        serde_yaml::Value::String("maximum".to_string()),
1582                        Self::json_to_yaml_value(maximum),
1583                    );
1584                }
1585                if let Some(ref exc_min) = opts.exclusive_minimum {
1586                    opts_map.insert(
1587                        serde_yaml::Value::String("exclusiveMinimum".to_string()),
1588                        Self::json_to_yaml_value(exc_min),
1589                    );
1590                }
1591                if let Some(ref exc_max) = opts.exclusive_maximum {
1592                    opts_map.insert(
1593                        serde_yaml::Value::String("exclusiveMaximum".to_string()),
1594                        Self::json_to_yaml_value(exc_max),
1595                    );
1596                }
1597                if let Some(precision) = opts.precision {
1598                    opts_map.insert(
1599                        serde_yaml::Value::String("precision".to_string()),
1600                        serde_yaml::Value::Number(serde_yaml::Number::from(precision)),
1601                    );
1602                }
1603                if let Some(scale) = opts.scale {
1604                    opts_map.insert(
1605                        serde_yaml::Value::String("scale".to_string()),
1606                        serde_yaml::Value::Number(serde_yaml::Number::from(scale)),
1607                    );
1608                }
1609                if !opts_map.is_empty() {
1610                    prop.insert(
1611                        serde_yaml::Value::String("logicalTypeOptions".to_string()),
1612                        serde_yaml::Value::Mapping(opts_map),
1613                    );
1614                }
1615            }
1616
1617            // primaryKeyPosition
1618            if let Some(pk_pos) = column.primary_key_position {
1619                prop.insert(
1620                    serde_yaml::Value::String("primaryKeyPosition".to_string()),
1621                    serde_yaml::Value::Number(serde_yaml::Number::from(pk_pos)),
1622                );
1623            }
1624
1625            // unique
1626            if column.unique {
1627                prop.insert(
1628                    serde_yaml::Value::String("unique".to_string()),
1629                    serde_yaml::Value::Bool(true),
1630                );
1631            }
1632
1633            // partitioned
1634            if column.partitioned {
1635                prop.insert(
1636                    serde_yaml::Value::String("partitioned".to_string()),
1637                    serde_yaml::Value::Bool(true),
1638                );
1639            }
1640
1641            // partitionKeyPosition
1642            if let Some(part_pos) = column.partition_key_position {
1643                prop.insert(
1644                    serde_yaml::Value::String("partitionKeyPosition".to_string()),
1645                    serde_yaml::Value::Number(serde_yaml::Number::from(part_pos)),
1646                );
1647            }
1648
1649            // classification
1650            if let Some(ref class) = column.classification {
1651                prop.insert(
1652                    serde_yaml::Value::String("classification".to_string()),
1653                    serde_yaml::Value::String(class.clone()),
1654                );
1655            }
1656
1657            // criticalDataElement
1658            if column.critical_data_element {
1659                prop.insert(
1660                    serde_yaml::Value::String("criticalDataElement".to_string()),
1661                    serde_yaml::Value::Bool(true),
1662                );
1663            }
1664
1665            // encryptedName
1666            if let Some(ref enc_name) = column.encrypted_name {
1667                prop.insert(
1668                    serde_yaml::Value::String("encryptedName".to_string()),
1669                    serde_yaml::Value::String(enc_name.clone()),
1670                );
1671            }
1672
1673            // transformSourceObjects
1674            if !column.transform_source_objects.is_empty() {
1675                let sources: Vec<serde_yaml::Value> = column
1676                    .transform_source_objects
1677                    .iter()
1678                    .map(|s| serde_yaml::Value::String(s.clone()))
1679                    .collect();
1680                prop.insert(
1681                    serde_yaml::Value::String("transformSourceObjects".to_string()),
1682                    serde_yaml::Value::Sequence(sources),
1683                );
1684            }
1685
1686            // transformLogic
1687            if let Some(ref logic) = column.transform_logic {
1688                prop.insert(
1689                    serde_yaml::Value::String("transformLogic".to_string()),
1690                    serde_yaml::Value::String(logic.clone()),
1691                );
1692            }
1693
1694            // transformDescription
1695            if let Some(ref desc) = column.transform_description {
1696                prop.insert(
1697                    serde_yaml::Value::String("transformDescription".to_string()),
1698                    serde_yaml::Value::String(desc.clone()),
1699                );
1700            }
1701
1702            // examples
1703            if !column.examples.is_empty() {
1704                let examples: Vec<serde_yaml::Value> = column
1705                    .examples
1706                    .iter()
1707                    .map(Self::json_to_yaml_value)
1708                    .collect();
1709                prop.insert(
1710                    serde_yaml::Value::String("examples".to_string()),
1711                    serde_yaml::Value::Sequence(examples),
1712                );
1713            }
1714
1715            // authoritativeDefinitions
1716            if !column.authoritative_definitions.is_empty() {
1717                let defs: Vec<serde_yaml::Value> = column
1718                    .authoritative_definitions
1719                    .iter()
1720                    .map(|d| {
1721                        let mut def_map = serde_yaml::Mapping::new();
1722                        def_map.insert(
1723                            serde_yaml::Value::String("type".to_string()),
1724                            serde_yaml::Value::String(d.definition_type.clone()),
1725                        );
1726                        def_map.insert(
1727                            serde_yaml::Value::String("url".to_string()),
1728                            serde_yaml::Value::String(d.url.clone()),
1729                        );
1730                        serde_yaml::Value::Mapping(def_map)
1731                    })
1732                    .collect();
1733                prop.insert(
1734                    serde_yaml::Value::String("authoritativeDefinitions".to_string()),
1735                    serde_yaml::Value::Sequence(defs),
1736                );
1737            }
1738
1739            // tags
1740            if !column.tags.is_empty() {
1741                let tags: Vec<serde_yaml::Value> = column
1742                    .tags
1743                    .iter()
1744                    .map(|t| serde_yaml::Value::String(t.clone()))
1745                    .collect();
1746                prop.insert(
1747                    serde_yaml::Value::String("tags".to_string()),
1748                    serde_yaml::Value::Sequence(tags),
1749                );
1750            }
1751
1752            // customProperties
1753            if !column.custom_properties.is_empty() {
1754                let custom_props: Vec<serde_yaml::Value> = column
1755                    .custom_properties
1756                    .iter()
1757                    .map(|(key, value)| {
1758                        let mut prop_map = serde_yaml::Mapping::new();
1759                        prop_map.insert(
1760                            serde_yaml::Value::String("property".to_string()),
1761                            serde_yaml::Value::String(key.clone()),
1762                        );
1763                        prop_map.insert(
1764                            serde_yaml::Value::String("value".to_string()),
1765                            Self::json_to_yaml_value(value),
1766                        );
1767                        serde_yaml::Value::Mapping(prop_map)
1768                    })
1769                    .collect();
1770                prop.insert(
1771                    serde_yaml::Value::String("customProperties".to_string()),
1772                    serde_yaml::Value::Sequence(custom_props),
1773                );
1774            }
1775
1776            // Export foreign key
1777            if let Some(ref fk) = column.foreign_key {
1778                let mut fk_map = serde_yaml::Mapping::new();
1779                fk_map.insert(
1780                    serde_yaml::Value::String("table".to_string()),
1781                    serde_yaml::Value::String(fk.table_id.clone()),
1782                );
1783                fk_map.insert(
1784                    serde_yaml::Value::String("column".to_string()),
1785                    serde_yaml::Value::String(fk.column_name.clone()),
1786                );
1787                prop.insert(
1788                    serde_yaml::Value::String("foreignKey".to_string()),
1789                    serde_yaml::Value::Mapping(fk_map),
1790                );
1791            }
1792
1793            // Add 'id' and 'name' fields to property object (required in ODCS v3.1.0)
1794            // Generate ID from name (convert to snake_case)
1795            let id = column
1796                .name
1797                .chars()
1798                .map(|c| {
1799                    if c.is_alphanumeric() {
1800                        c.to_lowercase().to_string()
1801                    } else {
1802                        "_".to_string()
1803                    }
1804                })
1805                .collect::<String>()
1806                .replace("__", "_");
1807
1808            prop.insert(
1809                serde_yaml::Value::String("id".to_string()),
1810                serde_yaml::Value::String(format!("{}_obj", id)),
1811            );
1812            prop.insert(
1813                serde_yaml::Value::String("name".to_string()),
1814                serde_yaml::Value::String(column.name.clone()),
1815            );
1816
1817            properties.push(serde_yaml::Value::Mapping(prop));
1818        }
1819
1820        schema_obj.insert(
1821            serde_yaml::Value::String("properties".to_string()),
1822            serde_yaml::Value::Sequence(properties),
1823        );
1824
1825        schema_array.push(serde_yaml::Value::Mapping(schema_obj));
1826        yaml.insert(
1827            serde_yaml::Value::String("schema".to_string()),
1828            serde_yaml::Value::Sequence(schema_array),
1829        );
1830
1831        // Table-level quality rules
1832        if !table.quality.is_empty() {
1833            let quality_array: Vec<serde_yaml::Value> = table
1834                .quality
1835                .iter()
1836                .map(|rule| {
1837                    let mut rule_map = serde_yaml::Mapping::new();
1838                    for (k, v) in rule {
1839                        rule_map.insert(
1840                            serde_yaml::Value::String(k.clone()),
1841                            Self::json_to_yaml_value(v),
1842                        );
1843                    }
1844                    serde_yaml::Value::Mapping(rule_map)
1845                })
1846                .collect();
1847            yaml.insert(
1848                serde_yaml::Value::String("quality".to_string()),
1849                serde_yaml::Value::Sequence(quality_array),
1850            );
1851        }
1852
1853        // Custom Properties from metadata (excluding already exported fields)
1854        let excluded_keys = [
1855            "id",
1856            "version",
1857            "status",
1858            "domain",
1859            "dataProduct",
1860            "tenant",
1861            "description",
1862            "team",
1863            "roles",
1864            "pricing",
1865            "terms",
1866            "servers",
1867            "servicelevels",
1868            "links",
1869            "apiVersion",
1870            "kind",
1871            "info",
1872            "dataContractSpecification",
1873        ];
1874
1875        let mut custom_props = Vec::new();
1876        for (key, value) in &table.odcl_metadata {
1877            if !excluded_keys.contains(&key.as_str()) && !value.is_null() {
1878                let mut prop = serde_yaml::Mapping::new();
1879                prop.insert(
1880                    serde_yaml::Value::String("property".to_string()),
1881                    serde_yaml::Value::String(key.clone()),
1882                );
1883                prop.insert(
1884                    serde_yaml::Value::String("value".to_string()),
1885                    Self::json_to_yaml_value(value),
1886                );
1887                custom_props.push(serde_yaml::Value::Mapping(prop));
1888            }
1889        }
1890
1891        // Add database type as custom property if present
1892        if let Some(ref db_type) = table.database_type {
1893            let mut prop = serde_yaml::Mapping::new();
1894            prop.insert(
1895                serde_yaml::Value::String("property".to_string()),
1896                serde_yaml::Value::String("databaseType".to_string()),
1897            );
1898            prop.insert(
1899                serde_yaml::Value::String("value".to_string()),
1900                serde_yaml::Value::String(format!("{:?}", db_type)),
1901            );
1902            custom_props.push(serde_yaml::Value::Mapping(prop));
1903        }
1904
1905        // Add medallion layers as custom property if present
1906        if !table.medallion_layers.is_empty() {
1907            let layers: Vec<serde_yaml::Value> = table
1908                .medallion_layers
1909                .iter()
1910                .map(|l| serde_yaml::Value::String(format!("{:?}", l)))
1911                .collect();
1912            let mut prop = serde_yaml::Mapping::new();
1913            prop.insert(
1914                serde_yaml::Value::String("property".to_string()),
1915                serde_yaml::Value::String("medallionLayers".to_string()),
1916            );
1917            prop.insert(
1918                serde_yaml::Value::String("value".to_string()),
1919                serde_yaml::Value::Sequence(layers),
1920            );
1921            custom_props.push(serde_yaml::Value::Mapping(prop));
1922        }
1923
1924        // Add SCD pattern as custom property if present
1925        if let Some(ref scd_pattern) = table.scd_pattern {
1926            let mut prop = serde_yaml::Mapping::new();
1927            prop.insert(
1928                serde_yaml::Value::String("property".to_string()),
1929                serde_yaml::Value::String("scdPattern".to_string()),
1930            );
1931            prop.insert(
1932                serde_yaml::Value::String("value".to_string()),
1933                serde_yaml::Value::String(format!("{:?}", scd_pattern)),
1934            );
1935            custom_props.push(serde_yaml::Value::Mapping(prop));
1936        }
1937
1938        // Add Data Vault classification as custom property if present
1939        if let Some(ref dv_class) = table.data_vault_classification {
1940            let mut prop = serde_yaml::Mapping::new();
1941            prop.insert(
1942                serde_yaml::Value::String("property".to_string()),
1943                serde_yaml::Value::String("dataVaultClassification".to_string()),
1944            );
1945            prop.insert(
1946                serde_yaml::Value::String("value".to_string()),
1947                serde_yaml::Value::String(format!("{:?}", dv_class)),
1948            );
1949            custom_props.push(serde_yaml::Value::Mapping(prop));
1950        }
1951
1952        // Add catalog/schema names as custom properties if present
1953        if let Some(ref catalog) = table.catalog_name {
1954            let mut prop = serde_yaml::Mapping::new();
1955            prop.insert(
1956                serde_yaml::Value::String("property".to_string()),
1957                serde_yaml::Value::String("catalogName".to_string()),
1958            );
1959            prop.insert(
1960                serde_yaml::Value::String("value".to_string()),
1961                serde_yaml::Value::String(catalog.clone()),
1962            );
1963            custom_props.push(serde_yaml::Value::Mapping(prop));
1964        }
1965
1966        if let Some(ref schema) = table.schema_name {
1967            let mut prop = serde_yaml::Mapping::new();
1968            prop.insert(
1969                serde_yaml::Value::String("property".to_string()),
1970                serde_yaml::Value::String("schemaName".to_string()),
1971            );
1972            prop.insert(
1973                serde_yaml::Value::String("value".to_string()),
1974                serde_yaml::Value::String(schema.clone()),
1975            );
1976            custom_props.push(serde_yaml::Value::Mapping(prop));
1977        }
1978
1979        if !custom_props.is_empty() {
1980            yaml.insert(
1981                serde_yaml::Value::String("customProperties".to_string()),
1982                serde_yaml::Value::Sequence(custom_props),
1983            );
1984        }
1985
1986        // Contract created timestamp
1987        yaml.insert(
1988            serde_yaml::Value::String("contractCreatedTs".to_string()),
1989            serde_yaml::Value::String(table.created_at.to_rfc3339()),
1990        );
1991
1992        serde_yaml::to_string(&yaml).unwrap_or_default()
1993    }
1994
1995    /// Export tables to ODCS v3.1.0 YAML format (SDK interface).
1996    pub fn export(
1997        &self,
1998        tables: &[Table],
1999        _format: &str,
2000    ) -> Result<HashMap<String, ExportResult>, ExportError> {
2001        let mut exports = HashMap::new();
2002        for table in tables {
2003            // All exports use ODCS v3.1.0 format
2004            let yaml = Self::export_odcs_v3_1_0_format(table);
2005
2006            // Validate exported YAML against ODCS schema (if feature enabled)
2007            #[cfg(feature = "schema-validation")]
2008            {
2009                use crate::validation::schema::validate_odcs_internal;
2010                validate_odcs_internal(&yaml).map_err(|e| {
2011                    ExportError::ValidationError(format!("ODCS validation failed: {}", e))
2012                })?;
2013            }
2014
2015            exports.insert(
2016                table.name.clone(),
2017                ExportResult {
2018                    content: yaml,
2019                    format: "odcs_v3_1_0".to_string(),
2020                },
2021            );
2022        }
2023        Ok(exports)
2024    }
2025
2026    /// Export a data model to ODCS v3.1.0 YAML format (legacy method for compatibility).
2027    pub fn export_model(
2028        model: &DataModel,
2029        table_ids: Option<&[uuid::Uuid]>,
2030        _format: &str,
2031    ) -> HashMap<String, String> {
2032        let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
2033            model
2034                .tables
2035                .iter()
2036                .filter(|t| ids.contains(&t.id))
2037                .collect()
2038        } else {
2039            model.tables.iter().collect()
2040        };
2041
2042        let mut exports = HashMap::new();
2043        for table in tables_to_export {
2044            // All exports use ODCS v3.1.0 format
2045            let yaml = Self::export_odcs_v3_1_0_format(table);
2046            exports.insert(table.name.clone(), yaml);
2047        }
2048
2049        exports
2050    }
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055    use super::*;
2056    use crate::models::{Column, Tag};
2057
2058    #[test]
2059    fn test_export_odcs_v3_1_0_basic() {
2060        let table = Table {
2061            id: Table::generate_id("test_table", None, None, None),
2062            name: "test_table".to_string(),
2063            columns: vec![Column {
2064                name: "id".to_string(),
2065                data_type: "BIGINT".to_string(),
2066                nullable: false,
2067                primary_key: true,
2068                description: "Primary key".to_string(),
2069                ..Default::default()
2070            }],
2071            database_type: None,
2072            catalog_name: None,
2073            schema_name: None,
2074            medallion_layers: Vec::new(),
2075            scd_pattern: None,
2076            data_vault_classification: None,
2077            modeling_level: None,
2078            tags: vec![Tag::Simple("test".to_string())],
2079            odcl_metadata: HashMap::new(),
2080            owner: None,
2081            sla: None,
2082            contact_details: None,
2083            infrastructure_type: None,
2084            notes: None,
2085            position: None,
2086            yaml_file_path: None,
2087            drawio_cell_id: None,
2088            quality: Vec::new(),
2089            errors: Vec::new(),
2090            created_at: chrono::Utc::now(),
2091            updated_at: chrono::Utc::now(),
2092        };
2093
2094        let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
2095
2096        assert!(yaml.contains("apiVersion: v3.1.0"));
2097        assert!(yaml.contains("kind: DataContract"));
2098        assert!(yaml.contains("name: test_table"));
2099        assert!(yaml.contains("tags:"));
2100        assert!(yaml.contains("- test"));
2101    }
2102}