bq_schema_gen/output/
mod.rs

1//! Output formatting for BigQuery schema.
2//!
3//! Supports multiple output formats:
4//! - JSON: Standard BigQuery schema format (default)
5//! - DDL: BigQuery CREATE TABLE statement
6//! - Debug Map: Internal schema representation for debugging
7//! - JSON Schema: JSON Schema draft-07 format
8
9use std::io::Write;
10
11use serde::Serialize;
12
13use crate::error::Result;
14use crate::schema::types::{BqType, EntryStatus, SchemaEntry, SchemaMap};
15use crate::schema::BqSchemaField;
16
17/// Output format for the generated schema.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum OutputFormat {
20    /// Standard BigQuery JSON schema format (default)
21    #[default]
22    Json,
23    /// BigQuery DDL (CREATE TABLE statement)
24    Ddl,
25    /// Debug map showing internal schema state
26    DebugMap,
27    /// JSON Schema draft-07 format
28    JsonSchema,
29}
30
31impl std::str::FromStr for OutputFormat {
32    type Err = String;
33
34    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
35        match s.to_lowercase().as_str() {
36            "json" => Ok(OutputFormat::Json),
37            "ddl" => Ok(OutputFormat::Ddl),
38            "debug-map" | "debug_map" | "debugmap" => Ok(OutputFormat::DebugMap),
39            "json-schema" | "json_schema" | "jsonschema" => Ok(OutputFormat::JsonSchema),
40            _ => Err(format!("Unknown output format: {}", s)),
41        }
42    }
43}
44
45// =============================================================================
46// JSON Output (Default)
47// =============================================================================
48
49/// Write the schema as pretty-printed JSON to the given writer.
50pub fn write_schema_json<W: Write>(schema: &[BqSchemaField], writer: &mut W) -> Result<()> {
51    let json = serde_json::to_string_pretty(schema)
52        .map_err(|e| crate::error::Error::SchemaFile(e.to_string()))?;
53    writeln!(writer, "{}", json)?;
54    Ok(())
55}
56
57/// Convert the schema to a JSON string.
58pub fn schema_to_json_string(schema: &[BqSchemaField]) -> Result<String> {
59    serde_json::to_string_pretty(schema).map_err(|e| crate::error::Error::SchemaFile(e.to_string()))
60}
61
62// =============================================================================
63// DDL Output
64// =============================================================================
65
66/// Write the schema as BigQuery DDL (CREATE TABLE statement).
67///
68/// Output format:
69/// ```sql
70/// CREATE TABLE `dataset.table_name` (
71///   field_name STRING,
72///   required_field INT64 NOT NULL,
73///   array_field ARRAY<STRING>,
74///   record_field STRUCT<nested STRING>
75/// );
76/// ```
77pub fn write_schema_ddl<W: Write>(
78    schema: &[BqSchemaField],
79    table_name: &str,
80    writer: &mut W,
81) -> Result<()> {
82    writeln!(writer, "CREATE TABLE `{}` (", table_name)?;
83
84    let fields: Vec<String> = schema.iter().map(field_to_ddl).collect();
85
86    for (i, field) in fields.iter().enumerate() {
87        if i < fields.len() - 1 {
88            writeln!(writer, "  {},", field)?;
89        } else {
90            writeln!(writer, "  {}", field)?;
91        }
92    }
93
94    writeln!(writer, ");")?;
95    Ok(())
96}
97
98/// Convert a single field to DDL format.
99fn field_to_ddl(field: &BqSchemaField) -> String {
100    let type_str = bq_type_to_standard_sql(&field.field_type);
101    let mode = field.mode.as_str();
102
103    match mode {
104        "REPEATED" => {
105            if field.field_type == "RECORD" {
106                let nested = field
107                    .fields
108                    .as_ref()
109                    .map(|f| fields_to_struct(f))
110                    .unwrap_or_default();
111                format!("{} ARRAY<STRUCT<{}>>", field.name, nested)
112            } else {
113                format!("{} ARRAY<{}>", field.name, type_str)
114            }
115        }
116        "REQUIRED" => {
117            if field.field_type == "RECORD" {
118                let nested = field
119                    .fields
120                    .as_ref()
121                    .map(|f| fields_to_struct(f))
122                    .unwrap_or_default();
123                format!("{} STRUCT<{}> NOT NULL", field.name, nested)
124            } else {
125                format!("{} {} NOT NULL", field.name, type_str)
126            }
127        }
128        _ => {
129            // NULLABLE
130            if field.field_type == "RECORD" {
131                let nested = field
132                    .fields
133                    .as_ref()
134                    .map(|f| fields_to_struct(f))
135                    .unwrap_or_default();
136                format!("{} STRUCT<{}>", field.name, nested)
137            } else {
138                format!("{} {}", field.name, type_str)
139            }
140        }
141    }
142}
143
144/// Convert nested fields to STRUCT notation.
145fn fields_to_struct(fields: &[BqSchemaField]) -> String {
146    fields
147        .iter()
148        .map(|f| {
149            let type_str = bq_type_to_standard_sql(&f.field_type);
150            if f.field_type == "RECORD" {
151                let nested = f
152                    .fields
153                    .as_ref()
154                    .map(|inner| fields_to_struct(inner))
155                    .unwrap_or_default();
156                if f.mode == "REPEATED" {
157                    format!("{} ARRAY<STRUCT<{}>>", f.name, nested)
158                } else {
159                    format!("{} STRUCT<{}>", f.name, nested)
160                }
161            } else if f.mode == "REPEATED" {
162                format!("{} ARRAY<{}>", f.name, type_str)
163            } else {
164                format!("{} {}", f.name, type_str)
165            }
166        })
167        .collect::<Vec<_>>()
168        .join(", ")
169}
170
171/// Convert legacy BigQuery type names to Standard SQL types.
172fn bq_type_to_standard_sql(legacy_type: &str) -> &'static str {
173    match legacy_type {
174        "INTEGER" => "INT64",
175        "FLOAT" => "FLOAT64",
176        "BOOLEAN" => "BOOL",
177        "STRING" => "STRING",
178        "BYTES" => "BYTES",
179        "TIMESTAMP" => "TIMESTAMP",
180        "DATE" => "DATE",
181        "TIME" => "TIME",
182        "DATETIME" => "DATETIME",
183        "RECORD" => "STRUCT",
184        _ => "STRING", // Fallback
185    }
186}
187
188// =============================================================================
189// Debug Map Output
190// =============================================================================
191
192/// Serializable representation of a schema entry for debug output.
193#[derive(Debug, Serialize)]
194struct DebugSchemaEntry {
195    status: String,
196    filled: bool,
197    name: String,
198    bq_type: String,
199    mode: String,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    fields: Option<serde_json::Value>,
202}
203
204impl From<&SchemaEntry> for DebugSchemaEntry {
205    fn from(entry: &SchemaEntry) -> Self {
206        let status = match entry.status {
207            EntryStatus::Hard => "Hard",
208            EntryStatus::Soft => "Soft",
209            EntryStatus::Ignore => "Ignore",
210        };
211
212        let fields = if let BqType::Record(map) = &entry.bq_type {
213            Some(schema_map_to_debug_value(map))
214        } else {
215            None
216        };
217
218        DebugSchemaEntry {
219            status: status.to_string(),
220            filled: entry.filled,
221            name: entry.name.clone(),
222            bq_type: entry.bq_type.as_str().to_string(),
223            mode: entry.mode.as_str().to_string(),
224            fields,
225        }
226    }
227}
228
229/// Convert a SchemaMap to debug JSON value.
230fn schema_map_to_debug_value(map: &SchemaMap) -> serde_json::Value {
231    let mut obj = serde_json::Map::new();
232    for (key, entry) in map {
233        let debug_entry = DebugSchemaEntry::from(entry);
234        obj.insert(
235            key.clone(),
236            serde_json::to_value(&debug_entry).unwrap_or(serde_json::Value::Null),
237        );
238    }
239    serde_json::Value::Object(obj)
240}
241
242/// Write the internal schema map as debug JSON output.
243///
244/// This shows the internal representation including entry status (Hard/Soft/Ignore),
245/// filled state, and other metadata useful for debugging.
246pub fn write_schema_debug_map<W: Write>(schema_map: &SchemaMap, writer: &mut W) -> Result<()> {
247    let debug_value = schema_map_to_debug_value(schema_map);
248    let json = serde_json::to_string_pretty(&debug_value)
249        .map_err(|e| crate::error::Error::SchemaFile(e.to_string()))?;
250    writeln!(writer, "{}", json)?;
251    Ok(())
252}
253
254// =============================================================================
255// JSON Schema Output
256// =============================================================================
257
258/// Write the schema as JSON Schema draft-07 format.
259pub fn write_schema_json_schema<W: Write>(schema: &[BqSchemaField], writer: &mut W) -> Result<()> {
260    let json_schema = bq_schema_to_json_schema(schema);
261    let json = serde_json::to_string_pretty(&json_schema)
262        .map_err(|e| crate::error::Error::SchemaFile(e.to_string()))?;
263    writeln!(writer, "{}", json)?;
264    Ok(())
265}
266
267/// Convert BigQuery schema to JSON Schema format.
268fn bq_schema_to_json_schema(schema: &[BqSchemaField]) -> serde_json::Value {
269    let mut properties = serde_json::Map::new();
270    let mut required = Vec::new();
271
272    for field in schema {
273        let (prop_schema, is_required) = field_to_json_schema(field);
274        properties.insert(field.name.clone(), prop_schema);
275        if is_required {
276            required.push(serde_json::Value::String(field.name.clone()));
277        }
278    }
279
280    let mut schema_obj = serde_json::Map::new();
281    schema_obj.insert(
282        "$schema".to_string(),
283        serde_json::Value::String("http://json-schema.org/draft-07/schema#".to_string()),
284    );
285    schema_obj.insert(
286        "type".to_string(),
287        serde_json::Value::String("object".to_string()),
288    );
289    schema_obj.insert(
290        "properties".to_string(),
291        serde_json::Value::Object(properties),
292    );
293    if !required.is_empty() {
294        schema_obj.insert("required".to_string(), serde_json::Value::Array(required));
295    }
296
297    serde_json::Value::Object(schema_obj)
298}
299
300/// Convert a single BigQuery field to JSON Schema property.
301/// Returns (schema, is_required).
302fn field_to_json_schema(field: &BqSchemaField) -> (serde_json::Value, bool) {
303    let is_required = field.mode == "REQUIRED";
304    let base_type = bq_type_to_json_schema_type(&field.field_type);
305
306    let schema = if field.mode == "REPEATED" {
307        // Array type
308        let mut arr_schema = serde_json::Map::new();
309        arr_schema.insert(
310            "type".to_string(),
311            serde_json::Value::String("array".to_string()),
312        );
313
314        if field.field_type == "RECORD" {
315            arr_schema.insert("items".to_string(), record_to_json_schema(field));
316        } else {
317            let mut items = serde_json::Map::new();
318            items.insert("type".to_string(), serde_json::Value::String(base_type));
319            arr_schema.insert("items".to_string(), serde_json::Value::Object(items));
320        }
321
322        serde_json::Value::Object(arr_schema)
323    } else if field.field_type == "RECORD" {
324        record_to_json_schema(field)
325    } else {
326        // Simple type
327        let mut prop = serde_json::Map::new();
328        prop.insert("type".to_string(), serde_json::Value::String(base_type));
329        serde_json::Value::Object(prop)
330    };
331
332    (schema, is_required)
333}
334
335/// Convert a RECORD field to JSON Schema object.
336fn record_to_json_schema(field: &BqSchemaField) -> serde_json::Value {
337    let mut obj = serde_json::Map::new();
338    obj.insert(
339        "type".to_string(),
340        serde_json::Value::String("object".to_string()),
341    );
342
343    if let Some(nested_fields) = &field.fields {
344        let mut properties = serde_json::Map::new();
345        let mut required = Vec::new();
346
347        for nested in nested_fields {
348            let (nested_schema, is_required) = field_to_json_schema(nested);
349            properties.insert(nested.name.clone(), nested_schema);
350            if is_required {
351                required.push(serde_json::Value::String(nested.name.clone()));
352            }
353        }
354
355        obj.insert(
356            "properties".to_string(),
357            serde_json::Value::Object(properties),
358        );
359        if !required.is_empty() {
360            obj.insert("required".to_string(), serde_json::Value::Array(required));
361        }
362    }
363
364    serde_json::Value::Object(obj)
365}
366
367/// Convert BigQuery type to JSON Schema type.
368fn bq_type_to_json_schema_type(bq_type: &str) -> String {
369    match bq_type {
370        "STRING" => "string",
371        "INTEGER" => "integer",
372        "FLOAT" => "number",
373        "BOOLEAN" => "boolean",
374        "TIMESTAMP" | "DATE" | "TIME" | "DATETIME" => "string", // DateTime types as strings
375        "BYTES" => "string",
376        "RECORD" => "object",
377        _ => "string",
378    }
379    .to_string()
380}
381
382// =============================================================================
383// Tests
384// =============================================================================
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    #[test]
391    fn test_write_schema_json() {
392        let schema = vec![BqSchemaField::new(
393            "test".to_string(),
394            "STRING".to_string(),
395            "NULLABLE".to_string(),
396        )];
397
398        let mut output = Vec::new();
399        write_schema_json(&schema, &mut output).unwrap();
400
401        let output_str = String::from_utf8(output).unwrap();
402        assert!(output_str.contains("\"name\": \"test\""));
403        assert!(output_str.contains("\"type\": \"STRING\""));
404        assert!(output_str.contains("\"mode\": \"NULLABLE\""));
405    }
406
407    #[test]
408    fn test_write_schema_ddl_simple() {
409        let schema = vec![
410            BqSchemaField::new(
411                "name".to_string(),
412                "STRING".to_string(),
413                "NULLABLE".to_string(),
414            ),
415            BqSchemaField::new(
416                "age".to_string(),
417                "INTEGER".to_string(),
418                "REQUIRED".to_string(),
419            ),
420        ];
421
422        let mut output = Vec::new();
423        write_schema_ddl(&schema, "my_dataset.my_table", &mut output).unwrap();
424
425        let output_str = String::from_utf8(output).unwrap();
426        assert!(output_str.contains("CREATE TABLE `my_dataset.my_table`"));
427        assert!(output_str.contains("name STRING"));
428        assert!(output_str.contains("age INT64 NOT NULL"));
429    }
430
431    #[test]
432    fn test_write_schema_ddl_array() {
433        let schema = vec![BqSchemaField::new(
434            "tags".to_string(),
435            "STRING".to_string(),
436            "REPEATED".to_string(),
437        )];
438
439        let mut output = Vec::new();
440        write_schema_ddl(&schema, "test.table", &mut output).unwrap();
441
442        let output_str = String::from_utf8(output).unwrap();
443        assert!(output_str.contains("tags ARRAY<STRING>"));
444    }
445
446    #[test]
447    fn test_write_schema_ddl_nested() {
448        let schema = vec![BqSchemaField::record(
449            "user".to_string(),
450            "NULLABLE".to_string(),
451            vec![
452                BqSchemaField::new(
453                    "email".to_string(),
454                    "STRING".to_string(),
455                    "NULLABLE".to_string(),
456                ),
457                BqSchemaField::new(
458                    "age".to_string(),
459                    "INTEGER".to_string(),
460                    "NULLABLE".to_string(),
461                ),
462            ],
463        )];
464
465        let mut output = Vec::new();
466        write_schema_ddl(&schema, "test.table", &mut output).unwrap();
467
468        let output_str = String::from_utf8(output).unwrap();
469        assert!(output_str.contains("user STRUCT<"));
470        assert!(output_str.contains("email STRING"));
471        assert!(output_str.contains("age INT64"));
472    }
473
474    #[test]
475    fn test_write_schema_json_schema() {
476        let schema = vec![
477            BqSchemaField::new(
478                "name".to_string(),
479                "STRING".to_string(),
480                "NULLABLE".to_string(),
481            ),
482            BqSchemaField::new(
483                "count".to_string(),
484                "INTEGER".to_string(),
485                "REQUIRED".to_string(),
486            ),
487        ];
488
489        let mut output = Vec::new();
490        write_schema_json_schema(&schema, &mut output).unwrap();
491
492        let output_str = String::from_utf8(output).unwrap();
493        assert!(output_str.contains("\"$schema\""));
494        assert!(output_str.contains("draft-07"));
495        assert!(output_str.contains("\"properties\""));
496        assert!(output_str.contains("\"name\""));
497        assert!(output_str.contains("\"type\": \"string\""));
498        assert!(output_str.contains("\"type\": \"integer\""));
499        assert!(output_str.contains("\"required\""));
500        assert!(output_str.contains("\"count\""));
501    }
502
503    #[test]
504    fn test_output_format_from_str() {
505        assert_eq!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
506        assert_eq!("ddl".parse::<OutputFormat>().unwrap(), OutputFormat::Ddl);
507        assert_eq!(
508            "debug-map".parse::<OutputFormat>().unwrap(),
509            OutputFormat::DebugMap
510        );
511        assert_eq!(
512            "json-schema".parse::<OutputFormat>().unwrap(),
513            OutputFormat::JsonSchema
514        );
515        assert!("invalid".parse::<OutputFormat>().is_err());
516    }
517
518    // ===== DDL Output Tests =====
519
520    #[test]
521    fn test_ddl_output_all_types() {
522        let schema = vec![
523            BqSchemaField::new(
524                "str_field".to_string(),
525                "STRING".to_string(),
526                "NULLABLE".to_string(),
527            ),
528            BqSchemaField::new(
529                "int_field".to_string(),
530                "INTEGER".to_string(),
531                "NULLABLE".to_string(),
532            ),
533            BqSchemaField::new(
534                "float_field".to_string(),
535                "FLOAT".to_string(),
536                "NULLABLE".to_string(),
537            ),
538            BqSchemaField::new(
539                "bool_field".to_string(),
540                "BOOLEAN".to_string(),
541                "NULLABLE".to_string(),
542            ),
543            BqSchemaField::new(
544                "ts_field".to_string(),
545                "TIMESTAMP".to_string(),
546                "NULLABLE".to_string(),
547            ),
548            BqSchemaField::new(
549                "date_field".to_string(),
550                "DATE".to_string(),
551                "NULLABLE".to_string(),
552            ),
553            BqSchemaField::new(
554                "time_field".to_string(),
555                "TIME".to_string(),
556                "NULLABLE".to_string(),
557            ),
558            BqSchemaField::new(
559                "datetime_field".to_string(),
560                "DATETIME".to_string(),
561                "NULLABLE".to_string(),
562            ),
563            BqSchemaField::new(
564                "bytes_field".to_string(),
565                "BYTES".to_string(),
566                "NULLABLE".to_string(),
567            ),
568        ];
569
570        let mut output = Vec::new();
571        write_schema_ddl(&schema, "test.all_types", &mut output).unwrap();
572        let output_str = String::from_utf8(output).unwrap();
573
574        assert!(output_str.contains("str_field STRING"));
575        assert!(output_str.contains("int_field INT64"));
576        assert!(output_str.contains("float_field FLOAT64"));
577        assert!(output_str.contains("bool_field BOOL"));
578        assert!(output_str.contains("ts_field TIMESTAMP"));
579        assert!(output_str.contains("date_field DATE"));
580        assert!(output_str.contains("time_field TIME"));
581        assert!(output_str.contains("datetime_field DATETIME"));
582        assert!(output_str.contains("bytes_field BYTES"));
583    }
584
585    #[test]
586    fn test_ddl_output_repeated_record() {
587        let schema = vec![BqSchemaField::record(
588            "items".to_string(),
589            "REPEATED".to_string(),
590            vec![
591                BqSchemaField::new(
592                    "id".to_string(),
593                    "INTEGER".to_string(),
594                    "NULLABLE".to_string(),
595                ),
596                BqSchemaField::new(
597                    "name".to_string(),
598                    "STRING".to_string(),
599                    "NULLABLE".to_string(),
600                ),
601            ],
602        )];
603
604        let mut output = Vec::new();
605        write_schema_ddl(&schema, "test.repeated_record", &mut output).unwrap();
606        let output_str = String::from_utf8(output).unwrap();
607
608        assert!(output_str.contains("ARRAY<STRUCT<"));
609        assert!(output_str.contains("id INT64"));
610        assert!(output_str.contains("name STRING"));
611    }
612
613    #[test]
614    fn test_ddl_output_deeply_nested_records() {
615        let schema = vec![BqSchemaField::record(
616            "level1".to_string(),
617            "NULLABLE".to_string(),
618            vec![BqSchemaField::record(
619                "level2".to_string(),
620                "NULLABLE".to_string(),
621                vec![BqSchemaField::record(
622                    "level3".to_string(),
623                    "NULLABLE".to_string(),
624                    vec![BqSchemaField::new(
625                        "deep_value".to_string(),
626                        "STRING".to_string(),
627                        "NULLABLE".to_string(),
628                    )],
629                )],
630            )],
631        )];
632
633        let mut output = Vec::new();
634        write_schema_ddl(&schema, "test.nested", &mut output).unwrap();
635        let output_str = String::from_utf8(output).unwrap();
636
637        assert!(output_str.contains("level1 STRUCT<"));
638        assert!(output_str.contains("level2 STRUCT<"));
639        assert!(output_str.contains("level3 STRUCT<"));
640        assert!(output_str.contains("deep_value STRING"));
641    }
642
643    #[test]
644    fn test_ddl_output_required_record() {
645        let schema = vec![BqSchemaField::record(
646            "required_record".to_string(),
647            "REQUIRED".to_string(),
648            vec![BqSchemaField::new(
649                "field".to_string(),
650                "STRING".to_string(),
651                "NULLABLE".to_string(),
652            )],
653        )];
654
655        let mut output = Vec::new();
656        write_schema_ddl(&schema, "test.required", &mut output).unwrap();
657        let output_str = String::from_utf8(output).unwrap();
658
659        assert!(output_str.contains("STRUCT<"));
660        assert!(output_str.contains("NOT NULL"));
661    }
662
663    #[test]
664    fn test_ddl_output_repeated_primitive() {
665        let schema = vec![
666            BqSchemaField::new(
667                "int_array".to_string(),
668                "INTEGER".to_string(),
669                "REPEATED".to_string(),
670            ),
671            BqSchemaField::new(
672                "str_array".to_string(),
673                "STRING".to_string(),
674                "REPEATED".to_string(),
675            ),
676        ];
677
678        let mut output = Vec::new();
679        write_schema_ddl(&schema, "test.arrays", &mut output).unwrap();
680        let output_str = String::from_utf8(output).unwrap();
681
682        assert!(output_str.contains("int_array ARRAY<INT64>"));
683        assert!(output_str.contains("str_array ARRAY<STRING>"));
684    }
685
686    #[test]
687    fn test_ddl_output_nested_repeated_in_record() {
688        let schema = vec![BqSchemaField::record(
689            "parent".to_string(),
690            "NULLABLE".to_string(),
691            vec![
692                BqSchemaField::new(
693                    "tags".to_string(),
694                    "STRING".to_string(),
695                    "REPEATED".to_string(),
696                ),
697                BqSchemaField::record(
698                    "children".to_string(),
699                    "REPEATED".to_string(),
700                    vec![BqSchemaField::new(
701                        "child_id".to_string(),
702                        "INTEGER".to_string(),
703                        "NULLABLE".to_string(),
704                    )],
705                ),
706            ],
707        )];
708
709        let mut output = Vec::new();
710        write_schema_ddl(&schema, "test.nested_arrays", &mut output).unwrap();
711        let output_str = String::from_utf8(output).unwrap();
712
713        assert!(output_str.contains("tags ARRAY<STRING>"));
714        assert!(output_str.contains("children ARRAY<STRUCT<"));
715    }
716
717    // ===== JSON Schema Output Tests =====
718
719    #[test]
720    fn test_json_schema_primitives_comprehensive() {
721        let schema = vec![
722            BqSchemaField::new(
723                "str".to_string(),
724                "STRING".to_string(),
725                "NULLABLE".to_string(),
726            ),
727            BqSchemaField::new(
728                "int".to_string(),
729                "INTEGER".to_string(),
730                "NULLABLE".to_string(),
731            ),
732            BqSchemaField::new(
733                "float".to_string(),
734                "FLOAT".to_string(),
735                "NULLABLE".to_string(),
736            ),
737            BqSchemaField::new(
738                "bool".to_string(),
739                "BOOLEAN".to_string(),
740                "NULLABLE".to_string(),
741            ),
742            BqSchemaField::new(
743                "bytes".to_string(),
744                "BYTES".to_string(),
745                "NULLABLE".to_string(),
746            ),
747        ];
748
749        let mut output = Vec::new();
750        write_schema_json_schema(&schema, &mut output).unwrap();
751        let output_str = String::from_utf8(output).unwrap();
752        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
753
754        assert_eq!(parsed["properties"]["str"]["type"], "string");
755        assert_eq!(parsed["properties"]["int"]["type"], "integer");
756        assert_eq!(parsed["properties"]["float"]["type"], "number");
757        assert_eq!(parsed["properties"]["bool"]["type"], "boolean");
758        assert_eq!(parsed["properties"]["bytes"]["type"], "string");
759    }
760
761    #[test]
762    fn test_json_schema_arrays() {
763        let schema = vec![
764            BqSchemaField::new(
765                "string_arr".to_string(),
766                "STRING".to_string(),
767                "REPEATED".to_string(),
768            ),
769            BqSchemaField::new(
770                "int_arr".to_string(),
771                "INTEGER".to_string(),
772                "REPEATED".to_string(),
773            ),
774        ];
775
776        let mut output = Vec::new();
777        write_schema_json_schema(&schema, &mut output).unwrap();
778        let output_str = String::from_utf8(output).unwrap();
779        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
780
781        assert_eq!(parsed["properties"]["string_arr"]["type"], "array");
782        assert_eq!(
783            parsed["properties"]["string_arr"]["items"]["type"],
784            "string"
785        );
786        assert_eq!(parsed["properties"]["int_arr"]["type"], "array");
787        assert_eq!(parsed["properties"]["int_arr"]["items"]["type"], "integer");
788    }
789
790    #[test]
791    fn test_json_schema_nested_records() {
792        let schema = vec![BqSchemaField::record(
793            "user".to_string(),
794            "NULLABLE".to_string(),
795            vec![
796                BqSchemaField::new(
797                    "name".to_string(),
798                    "STRING".to_string(),
799                    "REQUIRED".to_string(),
800                ),
801                BqSchemaField::record(
802                    "address".to_string(),
803                    "NULLABLE".to_string(),
804                    vec![
805                        BqSchemaField::new(
806                            "city".to_string(),
807                            "STRING".to_string(),
808                            "NULLABLE".to_string(),
809                        ),
810                        BqSchemaField::new(
811                            "zip".to_string(),
812                            "STRING".to_string(),
813                            "NULLABLE".to_string(),
814                        ),
815                    ],
816                ),
817            ],
818        )];
819
820        let mut output = Vec::new();
821        write_schema_json_schema(&schema, &mut output).unwrap();
822        let output_str = String::from_utf8(output).unwrap();
823        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
824
825        assert_eq!(parsed["properties"]["user"]["type"], "object");
826        assert_eq!(
827            parsed["properties"]["user"]["properties"]["name"]["type"],
828            "string"
829        );
830        assert_eq!(
831            parsed["properties"]["user"]["properties"]["address"]["type"],
832            "object"
833        );
834        assert!(parsed["properties"]["user"]["required"]
835            .as_array()
836            .unwrap()
837            .contains(&serde_json::json!("name")));
838    }
839
840    #[test]
841    fn test_json_schema_date_time_formats() {
842        let schema = vec![
843            BqSchemaField::new(
844                "date_field".to_string(),
845                "DATE".to_string(),
846                "NULLABLE".to_string(),
847            ),
848            BqSchemaField::new(
849                "time_field".to_string(),
850                "TIME".to_string(),
851                "NULLABLE".to_string(),
852            ),
853            BqSchemaField::new(
854                "ts_field".to_string(),
855                "TIMESTAMP".to_string(),
856                "NULLABLE".to_string(),
857            ),
858            BqSchemaField::new(
859                "dt_field".to_string(),
860                "DATETIME".to_string(),
861                "NULLABLE".to_string(),
862            ),
863        ];
864
865        let mut output = Vec::new();
866        write_schema_json_schema(&schema, &mut output).unwrap();
867        let output_str = String::from_utf8(output).unwrap();
868        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
869
870        // All date/time types should be represented as strings in JSON Schema
871        assert_eq!(parsed["properties"]["date_field"]["type"], "string");
872        assert_eq!(parsed["properties"]["time_field"]["type"], "string");
873        assert_eq!(parsed["properties"]["ts_field"]["type"], "string");
874        assert_eq!(parsed["properties"]["dt_field"]["type"], "string");
875    }
876
877    #[test]
878    fn test_json_schema_repeated_records() {
879        let schema = vec![BqSchemaField::record(
880            "items".to_string(),
881            "REPEATED".to_string(),
882            vec![
883                BqSchemaField::new(
884                    "id".to_string(),
885                    "INTEGER".to_string(),
886                    "REQUIRED".to_string(),
887                ),
888                BqSchemaField::new(
889                    "value".to_string(),
890                    "FLOAT".to_string(),
891                    "NULLABLE".to_string(),
892                ),
893            ],
894        )];
895
896        let mut output = Vec::new();
897        write_schema_json_schema(&schema, &mut output).unwrap();
898        let output_str = String::from_utf8(output).unwrap();
899        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
900
901        assert_eq!(parsed["properties"]["items"]["type"], "array");
902        assert_eq!(parsed["properties"]["items"]["items"]["type"], "object");
903        assert_eq!(
904            parsed["properties"]["items"]["items"]["properties"]["id"]["type"],
905            "integer"
906        );
907        assert!(parsed["properties"]["items"]["items"]["required"]
908            .as_array()
909            .unwrap()
910            .contains(&serde_json::json!("id")));
911    }
912
913    #[test]
914    fn test_json_schema_required_fields_at_root() {
915        let schema = vec![
916            BqSchemaField::new(
917                "required_field".to_string(),
918                "STRING".to_string(),
919                "REQUIRED".to_string(),
920            ),
921            BqSchemaField::new(
922                "optional_field".to_string(),
923                "STRING".to_string(),
924                "NULLABLE".to_string(),
925            ),
926        ];
927
928        let mut output = Vec::new();
929        write_schema_json_schema(&schema, &mut output).unwrap();
930        let output_str = String::from_utf8(output).unwrap();
931        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
932
933        let required = parsed["required"].as_array().unwrap();
934        assert!(required.contains(&serde_json::json!("required_field")));
935        assert!(!required.contains(&serde_json::json!("optional_field")));
936    }
937
938    #[test]
939    fn test_json_schema_empty_schema() {
940        let schema: Vec<BqSchemaField> = vec![];
941
942        let mut output = Vec::new();
943        write_schema_json_schema(&schema, &mut output).unwrap();
944        let output_str = String::from_utf8(output).unwrap();
945        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
946
947        assert_eq!(parsed["type"], "object");
948        assert!(parsed["properties"].as_object().unwrap().is_empty());
949        assert!(parsed.get("required").is_none());
950    }
951
952    // ===== Debug Map Output Tests =====
953
954    #[test]
955    fn test_debug_map_output() {
956        use crate::schema::types::{BqMode, BqType, EntryStatus, SchemaEntry, SchemaMap};
957
958        let mut schema_map = SchemaMap::new();
959        schema_map.insert(
960            "test_field".to_string(),
961            SchemaEntry {
962                status: EntryStatus::Hard,
963                filled: true,
964                name: "test_field".to_string(),
965                bq_type: BqType::String,
966                mode: BqMode::Nullable,
967            },
968        );
969
970        let mut output = Vec::new();
971        write_schema_debug_map(&schema_map, &mut output).unwrap();
972        let output_str = String::from_utf8(output).unwrap();
973        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
974
975        assert!(parsed["test_field"]["status"]
976            .as_str()
977            .unwrap()
978            .contains("Hard"));
979        assert_eq!(parsed["test_field"]["filled"], true);
980        assert_eq!(parsed["test_field"]["bq_type"], "STRING");
981        assert_eq!(parsed["test_field"]["mode"], "NULLABLE");
982    }
983
984    #[test]
985    fn test_debug_map_with_record() {
986        use crate::schema::types::{BqMode, BqType, EntryStatus, SchemaEntry, SchemaMap};
987
988        let mut nested_map = SchemaMap::new();
989        nested_map.insert(
990            "nested_field".to_string(),
991            SchemaEntry {
992                status: EntryStatus::Hard,
993                filled: true,
994                name: "nested_field".to_string(),
995                bq_type: BqType::Integer,
996                mode: BqMode::Nullable,
997            },
998        );
999
1000        let mut schema_map = SchemaMap::new();
1001        schema_map.insert(
1002            "record_field".to_string(),
1003            SchemaEntry {
1004                status: EntryStatus::Hard,
1005                filled: true,
1006                name: "record_field".to_string(),
1007                bq_type: BqType::Record(nested_map),
1008                mode: BqMode::Nullable,
1009            },
1010        );
1011
1012        let mut output = Vec::new();
1013        write_schema_debug_map(&schema_map, &mut output).unwrap();
1014        let output_str = String::from_utf8(output).unwrap();
1015        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
1016
1017        assert!(parsed["record_field"]["fields"].is_object());
1018        assert!(parsed["record_field"]["fields"]["nested_field"].is_object());
1019    }
1020
1021    #[test]
1022    fn test_debug_map_all_entry_statuses() {
1023        use crate::schema::types::{BqMode, BqType, EntryStatus, SchemaEntry, SchemaMap};
1024
1025        let mut schema_map = SchemaMap::new();
1026        schema_map.insert(
1027            "hard".to_string(),
1028            SchemaEntry {
1029                status: EntryStatus::Hard,
1030                filled: true,
1031                name: "hard".to_string(),
1032                bq_type: BqType::String,
1033                mode: BqMode::Nullable,
1034            },
1035        );
1036        schema_map.insert(
1037            "soft".to_string(),
1038            SchemaEntry {
1039                status: EntryStatus::Soft,
1040                filled: false,
1041                name: "soft".to_string(),
1042                bq_type: BqType::Null,
1043                mode: BqMode::Nullable,
1044            },
1045        );
1046        schema_map.insert(
1047            "ignore".to_string(),
1048            SchemaEntry {
1049                status: EntryStatus::Ignore,
1050                filled: false,
1051                name: "ignore".to_string(),
1052                bq_type: BqType::String,
1053                mode: BqMode::Nullable,
1054            },
1055        );
1056
1057        let mut output = Vec::new();
1058        write_schema_debug_map(&schema_map, &mut output).unwrap();
1059        let output_str = String::from_utf8(output).unwrap();
1060        let parsed: serde_json::Value = serde_json::from_str(&output_str).unwrap();
1061
1062        assert!(parsed["hard"]["status"].as_str().unwrap().contains("Hard"));
1063        assert!(parsed["soft"]["status"].as_str().unwrap().contains("Soft"));
1064        assert!(parsed["ignore"]["status"]
1065            .as_str()
1066            .unwrap()
1067            .contains("Ignore"));
1068    }
1069
1070    // ===== Output Format Parsing Tests =====
1071
1072    #[test]
1073    fn test_output_format_case_insensitive() {
1074        assert_eq!("JSON".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
1075        assert_eq!("Json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
1076        assert_eq!("DDL".parse::<OutputFormat>().unwrap(), OutputFormat::Ddl);
1077        assert_eq!(
1078            "DEBUG-MAP".parse::<OutputFormat>().unwrap(),
1079            OutputFormat::DebugMap
1080        );
1081        assert_eq!(
1082            "JSON-SCHEMA".parse::<OutputFormat>().unwrap(),
1083            OutputFormat::JsonSchema
1084        );
1085    }
1086
1087    #[test]
1088    fn test_output_format_debug_map_variants() {
1089        assert_eq!(
1090            "debug-map".parse::<OutputFormat>().unwrap(),
1091            OutputFormat::DebugMap
1092        );
1093        assert_eq!(
1094            "debug_map".parse::<OutputFormat>().unwrap(),
1095            OutputFormat::DebugMap
1096        );
1097        assert_eq!(
1098            "debugmap".parse::<OutputFormat>().unwrap(),
1099            OutputFormat::DebugMap
1100        );
1101    }
1102
1103    #[test]
1104    fn test_output_format_json_schema_variants() {
1105        assert_eq!(
1106            "json-schema".parse::<OutputFormat>().unwrap(),
1107            OutputFormat::JsonSchema
1108        );
1109        assert_eq!(
1110            "json_schema".parse::<OutputFormat>().unwrap(),
1111            OutputFormat::JsonSchema
1112        );
1113        assert_eq!(
1114            "jsonschema".parse::<OutputFormat>().unwrap(),
1115            OutputFormat::JsonSchema
1116        );
1117    }
1118
1119    #[test]
1120    fn test_schema_to_json_string() {
1121        let schema = vec![BqSchemaField::new(
1122            "test".to_string(),
1123            "STRING".to_string(),
1124            "NULLABLE".to_string(),
1125        )];
1126
1127        let json_str = schema_to_json_string(&schema).unwrap();
1128        assert!(json_str.contains("\"name\": \"test\""));
1129        assert!(json_str.contains("\"type\": \"STRING\""));
1130    }
1131
1132    #[test]
1133    fn test_bq_type_to_standard_sql_fallback() {
1134        // Test the fallback case for unknown types
1135        let result = bq_type_to_standard_sql("UNKNOWN_TYPE");
1136        assert_eq!(result, "STRING");
1137    }
1138}