1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum OutputFormat {
20 #[default]
22 Json,
23 Ddl,
25 DebugMap,
27 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
45pub 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
57pub 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
62pub 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
98fn 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 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
144fn 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
171fn 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", }
186}
187
188#[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
229fn 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
242pub 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
254pub 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
267fn 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
300fn 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 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 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
335fn 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
367fn 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", "BYTES" => "string",
376 "RECORD" => "object",
377 _ => "string",
378 }
379 .to_string()
380}
381
382#[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 #[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 #[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 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 #[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 #[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 let result = bq_type_to_standard_sql("UNKNOWN_TYPE");
1136 assert_eq!(result, "STRING");
1137 }
1138}