1use std::fmt::Write as _;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ColumnSchema {
39 pub name: String,
41 pub rust_type: String,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TableSchema {
48 pub name: String,
50 pub columns: Vec<ColumnSchema>,
52}
53
54pub struct SchemaGenerator {
56 header: String,
58 emit_use: bool,
60 emit_model_structs: bool,
65 model_derives: String,
70 crate_prefix: String,
75}
76
77impl Default for SchemaGenerator {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83impl SchemaGenerator {
84 pub fn new() -> Self {
86 Self {
87 header: format!(
88 "// Auto-generated by sz-orm-cli generate schema at {}\n\
89 // DO NOT EDIT MANUALLY — re-run the command to refresh.\n\
90 //\n\
91 // This file contains typed_query! table declarations\n\
92 // enabling compile-time column-name verification.\n",
93 chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
94 ),
95 emit_use: true,
96 emit_model_structs: false,
97 model_derives: "Debug, Clone, serde::Serialize, serde::Deserialize".to_string(),
98 crate_prefix: "sz_orm_core".to_string(),
99 }
100 }
101
102 pub fn with_crate_prefix(mut self, prefix: impl Into<String>) -> Self {
107 self.crate_prefix = prefix.into();
108 self
109 }
110
111 pub fn with_header(mut self, header: impl Into<String>) -> Self {
113 self.header = header.into();
114 self
115 }
116
117 pub fn emit_use(mut self, emit: bool) -> Self {
119 self.emit_use = emit;
120 self
121 }
122
123 pub fn emit_model_structs(mut self, emit: bool) -> Self {
140 self.emit_model_structs = emit;
141 self
142 }
143
144 pub fn with_model_derives(mut self, derives: impl Into<String>) -> Self {
149 self.model_derives = derives.into();
150 self
151 }
152
153 pub fn generate(&self, tables: &[TableSchema]) -> String {
155 let mut out = String::new();
156
157 let _ = writeln!(out, "{}", self.header);
159 let _ = writeln!(out);
160
161 if self.emit_use {
163 let _ = writeln!(out, "use {}::typed_query;", self.crate_prefix);
164 if self.emit_model_structs
167 && !self.model_derives.is_empty()
168 && self.model_derives.contains("serde::")
169 {
170 let _ = writeln!(out, "use serde::{{Serialize, Deserialize}};");
171 }
172 let _ = writeln!(out);
173 }
174
175 if self.emit_model_structs {
177 for (idx, table) in tables.iter().enumerate() {
178 if idx > 0 {
179 let _ = writeln!(out);
180 }
181 let _ = write!(out, "{}", self.render_model_struct(table));
182 }
183 if !tables.is_empty() {
185 let _ = writeln!(out);
186 let _ = writeln!(out);
187 }
188 }
189
190 for (idx, table) in tables.iter().enumerate() {
192 if idx > 0 {
193 let _ = writeln!(out);
194 }
195 let _ = write!(out, "{}", self.render_table(table));
196 }
197
198 out
199 }
200
201 fn render_table(&self, table: &TableSchema) -> String {
203 let mut out = String::new();
204 let _ = writeln!(out, "typed_query! {{");
205 let _ = writeln!(out, " table {} {{", table.name);
206 for col in &table.columns {
207 let _ = writeln!(out, " {}: {},", col.name, col.rust_type);
208 }
209 let _ = writeln!(out, " }}");
210 let _ = writeln!(out, "}}");
211 out
212 }
213
214 fn render_model_struct(&self, table: &TableSchema) -> String {
219 let struct_name = to_pascal_case(&table.name);
220 let mut out = String::new();
221 if !self.model_derives.is_empty() {
223 let _ = writeln!(out, "#[derive({})]", self.model_derives);
224 }
225 let _ = writeln!(out, "#[table_name = \"{}\"]", table.name);
227 let _ = writeln!(out, "pub struct {} {{", struct_name);
228 for col in &table.columns {
229 let _ = writeln!(out, " pub {}: {},", col.name, col.rust_type);
230 }
231 let _ = writeln!(out, "}}");
232 out
233 }
234}
235
236fn to_pascal_case(input: &str) -> String {
241 let mut result = String::with_capacity(input.len());
242 let mut next_upper = true;
243 for ch in input.chars() {
244 if ch == '_' || ch == '-' || ch == ' ' {
245 next_upper = true;
246 continue;
247 }
248 if next_upper {
249 result.push(ch.to_ascii_uppercase());
250 next_upper = false;
251 } else {
252 result.push(ch);
253 }
254 }
255 result
256}
257
258pub fn sql_type_to_rust(sql_type: &str, nullable: bool) -> String {
275 let lower = sql_type.to_lowercase();
277 let after_param_strip = lower.split('(').next().unwrap_or(&lower).trim();
279 let base_type = after_param_strip
282 .strip_suffix(" unsigned")
283 .or_else(|| after_param_strip.strip_suffix(" zerofill"))
284 .unwrap_or(after_param_strip)
285 .trim();
286
287 let rust = match base_type {
288 "tinyint" => "i8",
290 "smallint" | "int2" | "smallserial" => "i16",
291 "bigint" | "int8" | "bigserial" => "i64",
292 "int" | "integer" | "int4" | "mediumint" | "serial" => "i32",
293 "float8" | "double" | "double precision" => "f64",
295 "float4" | "real" | "float" => "f32",
296 "decimal" | "numeric" => "f64",
297 "bool" | "boolean" => "bool",
299 "blob" | "bytea" | "binary" | "varbinary" | "tinyblob" | "mediumblob" | "longblob" => {
301 "Vec<u8>"
302 }
303 "date" => "String",
305 "datetime" | "timestamp" | "timestamptz" => "String",
306 "time" | "timetz" => "String",
307 "interval" => "String",
308 "json" | "jsonb" => "String",
310 "uuid" => "String",
312 "char" | "varchar" | "text" | "tinytext" | "mediumtext" | "longtext" => "String",
314 "enum" | "set" => "String",
315 _ => "String",
317 };
318
319 if nullable {
320 format!("Option<{}>", rust)
321 } else {
322 rust.to_string()
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn test_sql_type_to_rust_int() {
332 assert_eq!(sql_type_to_rust("INT", false), "i32");
333 assert_eq!(sql_type_to_rust("BIGINT", false), "i64");
334 assert_eq!(sql_type_to_rust("SMALLINT", false), "i16");
335 assert_eq!(sql_type_to_rust("TINYINT", false), "i8");
336 }
337
338 #[test]
339 fn test_sql_type_to_rust_float() {
340 assert_eq!(sql_type_to_rust("FLOAT", false), "f32");
341 assert_eq!(sql_type_to_rust("DOUBLE", false), "f64");
342 assert_eq!(sql_type_to_rust("DECIMAL(10,2)", false), "f64");
343 }
344
345 #[test]
346 fn test_sql_type_to_rust_bool() {
347 assert_eq!(sql_type_to_rust("BOOLEAN", false), "bool");
348 assert_eq!(sql_type_to_rust("TINYINT(1)", false), "i8");
349 }
350
351 #[test]
352 fn test_sql_type_to_rust_string() {
353 assert_eq!(sql_type_to_rust("VARCHAR(255)", false), "String");
354 assert_eq!(sql_type_to_rust("TEXT", false), "String");
355 assert_eq!(sql_type_to_rust("CHAR(36)", false), "String");
356 }
357
358 #[test]
359 fn test_sql_type_to_rust_binary() {
360 assert_eq!(sql_type_to_rust("BLOB", false), "Vec<u8>");
361 assert_eq!(sql_type_to_rust("BYTEA", false), "Vec<u8>");
362 }
363
364 #[test]
365 fn test_sql_type_to_rust_nullable() {
366 assert_eq!(sql_type_to_rust("INT", true), "Option<i32>");
367 assert_eq!(sql_type_to_rust("VARCHAR(255)", true), "Option<String>");
368 }
369
370 #[test]
371 fn test_sql_type_to_rust_pg_types() {
372 assert_eq!(sql_type_to_rust("int8", false), "i64");
373 assert_eq!(sql_type_to_rust("int2", false), "i16");
374 assert_eq!(sql_type_to_rust("float8", false), "f64");
375 assert_eq!(sql_type_to_rust("float4", false), "f32");
376 assert_eq!(sql_type_to_rust("bytea", false), "Vec<u8>");
377 }
378
379 #[test]
380 fn test_sql_type_to_rust_json_uuid() {
381 assert_eq!(sql_type_to_rust("JSON", false), "String");
382 assert_eq!(sql_type_to_rust("JSONB", false), "String");
383 assert_eq!(sql_type_to_rust("UUID", false), "String");
384 }
385
386 #[test]
387 fn test_schema_generator_single_table() {
388 let gen = SchemaGenerator::new().emit_use(false);
389 let tables = vec![TableSchema {
390 name: "users".to_string(),
391 columns: vec![
392 ColumnSchema {
393 name: "id".to_string(),
394 rust_type: "i64".to_string(),
395 },
396 ColumnSchema {
397 name: "name".to_string(),
398 rust_type: "String".to_string(),
399 },
400 ],
401 }];
402 let output = gen.generate(&tables);
403
404 assert!(output.contains("table users {"));
405 assert!(output.contains("id: i64,"));
406 assert!(output.contains("name: String,"));
407 assert!(output.contains("typed_query! {"));
408 }
409
410 #[test]
411 fn test_schema_generator_multiple_tables() {
412 let gen = SchemaGenerator::new().emit_use(false);
413 let tables = vec![
414 TableSchema {
415 name: "users".to_string(),
416 columns: vec![ColumnSchema {
417 name: "id".to_string(),
418 rust_type: "i64".to_string(),
419 }],
420 },
421 TableSchema {
422 name: "orders".to_string(),
423 columns: vec![ColumnSchema {
424 name: "order_id".to_string(),
425 rust_type: "i64".to_string(),
426 }],
427 },
428 ];
429 let output = gen.generate(&tables);
430
431 assert!(output.contains("table users {"));
432 assert!(output.contains("table orders {"));
433 let users_end = output.find("}").unwrap();
435 let orders_start = output.find("table orders").unwrap();
436 let between = &output[users_end..orders_start];
437 assert!(between.contains("\n\n"));
438 }
439
440 #[test]
441 fn test_schema_generator_with_use_statement() {
442 let gen = SchemaGenerator::new().emit_use(true);
443 let tables = vec![TableSchema {
444 name: "t".to_string(),
445 columns: vec![],
446 }];
447 let output = gen.generate(&tables);
448
449 assert!(output.contains("use sz_orm_core::typed_query;"));
450 }
451
452 #[test]
454 fn test_schema_generator_custom_crate_prefix() {
455 let gen = SchemaGenerator::new()
456 .emit_use(true)
457 .with_crate_prefix("sz_orm_query");
458 let tables = vec![TableSchema {
459 name: "t".to_string(),
460 columns: vec![],
461 }];
462 let output = gen.generate(&tables);
463
464 assert!(output.contains("use sz_orm_query::typed_query;"));
465 assert!(!output.contains("use sz_orm_core::typed_query;"));
466 }
467
468 #[test]
469 fn test_schema_generator_header() {
470 let gen = SchemaGenerator::new();
471 let output = gen.generate(&[]);
472 assert!(output.contains("Auto-generated"));
473 assert!(output.contains("DO NOT EDIT MANUALLY"));
474 }
475
476 #[test]
477 fn test_schema_generator_custom_header() {
478 let gen = SchemaGenerator::new().with_header("// Custom header\n");
479 let output = gen.generate(&[]);
480 assert!(output.starts_with("// Custom header"));
481 assert!(!output.contains("Auto-generated"));
482 }
483
484 #[test]
485 fn test_schema_generator_empty_tables() {
486 let gen = SchemaGenerator::new().emit_use(false);
487 let output = gen.generate(&[]);
488 assert!(output.contains("Auto-generated"));
490 assert!(!output.contains("typed_query! {"));
493 }
494
495 #[test]
496 fn test_schema_generator_option_type() {
497 let gen = SchemaGenerator::new().emit_use(false);
498 let tables = vec![TableSchema {
499 name: "products".to_string(),
500 columns: vec![ColumnSchema {
501 name: "price".to_string(),
502 rust_type: "Option<f64>".to_string(),
503 }],
504 }];
505 let output = gen.generate(&tables);
506
507 assert!(output.contains("price: Option<f64>,"));
508 }
509
510 #[test]
511 fn test_schema_generator_compound_type() {
512 let gen = SchemaGenerator::new().emit_use(false);
513 let tables = vec![TableSchema {
514 name: "files".to_string(),
515 columns: vec![ColumnSchema {
516 name: "content".to_string(),
517 rust_type: "Vec<u8>".to_string(),
518 }],
519 }];
520 let output = gen.generate(&tables);
521
522 assert!(output.contains("content: Vec<u8>,"));
523 }
524
525 #[test]
526 fn test_generated_code_is_valid_syntax() {
527 let gen = SchemaGenerator::new().emit_use(false);
529 let tables = vec![TableSchema {
530 name: "typed_validate_test".to_string(),
531 columns: vec![
532 ColumnSchema {
533 name: "id".to_string(),
534 rust_type: "i64".to_string(),
535 },
536 ColumnSchema {
537 name: "name".to_string(),
538 rust_type: "String".to_string(),
539 },
540 ],
541 }];
542 let output = gen.generate(&tables);
543
544 assert!(output.contains("typed_query! {"));
546 assert!(output.contains("table typed_validate_test {"));
547 assert!(output.contains("id: i64,"));
548 assert!(output.contains("name: String,"));
549 let count_open = output.matches("typed_query! {").count();
551 let count_close = output.matches("}\n}").count();
552 assert_eq!(count_open, 1);
553 assert_eq!(count_close, 1);
554 }
555
556 #[test]
559 fn test_sql_type_to_rust_interval_not_int() {
560 assert_eq!(sql_type_to_rust("INTERVAL", false), "String");
562 assert_eq!(sql_type_to_rust("interval", false), "String");
563 assert_eq!(sql_type_to_rust("INTERVAL DAY TO SECOND", false), "String");
565 }
566
567 #[test]
568 fn test_sql_type_to_rust_mediumint() {
569 assert_eq!(sql_type_to_rust("MEDIUMINT", false), "i32");
571 assert_eq!(sql_type_to_rust("MEDIUMINT(8)", false), "i32");
572 }
573
574 #[test]
575 fn test_sql_type_to_rust_integer_alias() {
576 assert_eq!(sql_type_to_rust("INTEGER", false), "i32");
578 assert_eq!(sql_type_to_rust("INTEGER(11)", false), "i32");
579 }
580
581 #[test]
582 fn test_sql_type_to_rust_serial_types() {
583 assert_eq!(sql_type_to_rust("SERIAL", false), "i32");
585 assert_eq!(sql_type_to_rust("BIGSERIAL", false), "i64");
586 assert_eq!(sql_type_to_rust("SMALLSERIAL", false), "i16");
587 }
588
589 #[test]
590 fn test_sql_type_to_rust_unsigned_suffix() {
591 assert_eq!(sql_type_to_rust("INT UNSIGNED", false), "i32");
593 assert_eq!(sql_type_to_rust("BIGINT UNSIGNED", false), "i64");
594 assert_eq!(sql_type_to_rust("TINYINT UNSIGNED", false), "i8");
595 assert_eq!(sql_type_to_rust("SMALLINT UNSIGNED", false), "i16");
596 assert_eq!(sql_type_to_rust("MEDIUMINT UNSIGNED", false), "i32");
597 }
598
599 #[test]
600 fn test_sql_type_to_rust_zerofill_suffix() {
601 assert_eq!(sql_type_to_rust("INT ZEROFILL", false), "i32");
603 assert_eq!(sql_type_to_rust("INT(4) ZEROFILL", false), "i32");
604 }
605
606 #[test]
607 fn test_sql_type_to_rust_timestamptz() {
608 assert_eq!(sql_type_to_rust("TIMESTAMPTZ", false), "String");
610 assert_eq!(sql_type_to_rust("timestamptz", false), "String");
611 }
612
613 #[test]
614 fn test_sql_type_to_rust_timetz() {
615 assert_eq!(sql_type_to_rust("TIMETZ", false), "String");
617 assert_eq!(sql_type_to_rust("timetz", false), "String");
618 }
619
620 #[test]
621 fn test_sql_type_to_rust_double_precision() {
622 assert_eq!(sql_type_to_rust("DOUBLE PRECISION", false), "f64");
624 assert_eq!(sql_type_to_rust("double precision", false), "f64");
625 }
626
627 #[test]
628 fn test_sql_type_to_rust_varbinary() {
629 assert_eq!(sql_type_to_rust("VARBINARY(255)", false), "Vec<u8>");
631 assert_eq!(sql_type_to_rust("VARBINARY", false), "Vec<u8>");
632 }
633
634 #[test]
635 fn test_sql_type_to_rust_blob_variants() {
636 assert_eq!(sql_type_to_rust("TINYBLOB", false), "Vec<u8>");
638 assert_eq!(sql_type_to_rust("MEDIUMBLOB", false), "Vec<u8>");
639 assert_eq!(sql_type_to_rust("LONGBLOB", false), "Vec<u8>");
640 }
641
642 #[test]
643 fn test_sql_type_to_rust_text_variants() {
644 assert_eq!(sql_type_to_rust("TINYTEXT", false), "String");
646 assert_eq!(sql_type_to_rust("MEDIUMTEXT", false), "String");
647 assert_eq!(sql_type_to_rust("LONGTEXT", false), "String");
648 }
649
650 #[test]
651 fn test_sql_type_to_rust_enum_and_set() {
652 assert_eq!(sql_type_to_rust("ENUM('a','b')", false), "String");
654 assert_eq!(sql_type_to_rust("SET('a','b')", false), "String");
655 }
656
657 #[test]
658 fn test_sql_type_to_rust_decimal_with_space() {
659 assert_eq!(sql_type_to_rust("DECIMAL(10, 2)", false), "f64");
661 assert_eq!(sql_type_to_rust("NUMERIC(8, 2)", false), "f64");
662 }
663
664 #[test]
665 fn test_sql_type_to_rust_unknown_defaults_string() {
666 assert_eq!(sql_type_to_rust("UNKNOWN_TYPE", false), "String");
668 assert_eq!(sql_type_to_rust("citext", false), "String");
669 assert_eq!(sql_type_to_rust("money", false), "String");
670 }
671
672 #[test]
673 fn test_sql_type_to_rust_interval_nullable() {
674 assert_eq!(sql_type_to_rust("INTERVAL", true), "Option<String>");
676 }
677
678 #[test]
683 fn test_to_pascal_case_basic() {
684 assert_eq!(to_pascal_case("users"), "Users");
685 assert_eq!(to_pascal_case("orders"), "Orders");
686 }
687
688 #[test]
689 fn test_to_pascal_case_snake_case() {
690 assert_eq!(to_pascal_case("order_items"), "OrderItems");
691 assert_eq!(to_pascal_case("user_profiles"), "UserProfiles");
692 }
693
694 #[test]
695 fn test_to_pascal_case_kebab_case() {
696 assert_eq!(to_pascal_case("user-profile"), "UserProfile");
697 assert_eq!(to_pascal_case("order-item"), "OrderItem");
698 }
699
700 #[test]
701 fn test_to_pascal_case_with_spaces() {
702 assert_eq!(to_pascal_case("user profile"), "UserProfile");
703 }
704
705 #[test]
706 fn test_to_pascal_case_single_char() {
707 assert_eq!(to_pascal_case("a"), "A");
708 assert_eq!(to_pascal_case("_a"), "A");
709 }
710
711 #[test]
712 fn test_to_pascal_case_already_pascal() {
713 assert_eq!(to_pascal_case("Users"), "Users");
715 }
716
717 #[test]
718 fn test_emit_model_structs_generates_struct() {
719 let gen = SchemaGenerator::new()
720 .emit_use(false)
721 .emit_model_structs(true);
722 let tables = vec![TableSchema {
723 name: "users".to_string(),
724 columns: vec![
725 ColumnSchema {
726 name: "id".to_string(),
727 rust_type: "i64".to_string(),
728 },
729 ColumnSchema {
730 name: "name".to_string(),
731 rust_type: "String".to_string(),
732 },
733 ],
734 }];
735 let output = gen.generate(&tables);
736
737 assert!(output.contains("pub struct Users {"));
739 assert!(output.contains("pub id: i64,"));
740 assert!(output.contains("pub name: String,"));
741 assert!(output.contains("#[derive("));
743 assert!(output.contains("Debug"));
744 assert!(output.contains("Clone"));
745 assert!(output.contains("#[table_name = \"users\"]"));
747 assert!(output.contains("typed_query! {"));
749 assert!(output.contains("table users {"));
750 }
751
752 #[test]
753 fn test_emit_model_structs_disabled_by_default() {
754 let gen = SchemaGenerator::new().emit_use(false);
755 let tables = vec![TableSchema {
756 name: "users".to_string(),
757 columns: vec![ColumnSchema {
758 name: "id".to_string(),
759 rust_type: "i64".to_string(),
760 }],
761 }];
762 let output = gen.generate(&tables);
763
764 assert!(!output.contains("pub struct Users"));
766 assert!(output.contains("typed_query! {"));
768 }
769
770 #[test]
771 fn test_emit_model_structs_multiple_tables() {
772 let gen = SchemaGenerator::new()
773 .emit_use(false)
774 .emit_model_structs(true);
775 let tables = vec![
776 TableSchema {
777 name: "users".to_string(),
778 columns: vec![ColumnSchema {
779 name: "id".to_string(),
780 rust_type: "i64".to_string(),
781 }],
782 },
783 TableSchema {
784 name: "order_items".to_string(),
785 columns: vec![ColumnSchema {
786 name: "item_id".to_string(),
787 rust_type: "i64".to_string(),
788 }],
789 },
790 ];
791 let output = gen.generate(&tables);
792
793 assert!(output.contains("pub struct Users {"));
795 assert!(output.contains("pub struct OrderItems {"));
796 }
797
798 #[test]
799 fn test_emit_model_structs_with_custom_derives() {
800 let gen = SchemaGenerator::new()
801 .emit_use(false)
802 .emit_model_structs(true)
803 .with_model_derives("Debug, Clone");
804 let tables = vec![TableSchema {
805 name: "users".to_string(),
806 columns: vec![ColumnSchema {
807 name: "id".to_string(),
808 rust_type: "i64".to_string(),
809 }],
810 }];
811 let output = gen.generate(&tables);
812
813 assert!(output.contains("#[derive(Debug, Clone)]"));
815 assert!(!output.contains("serde::Serialize"));
817 }
818
819 #[test]
820 fn test_emit_model_structs_with_empty_derives() {
821 let gen = SchemaGenerator::new()
822 .emit_use(false)
823 .emit_model_structs(true)
824 .with_model_derives("");
825 let tables = vec![TableSchema {
826 name: "users".to_string(),
827 columns: vec![ColumnSchema {
828 name: "id".to_string(),
829 rust_type: "i64".to_string(),
830 }],
831 }];
832 let output = gen.generate(&tables);
833
834 assert!(!output.contains("#[derive("));
836 assert!(output.contains("pub struct Users {"));
838 }
839
840 #[test]
841 fn test_emit_model_structs_with_nullable_fields() {
842 let gen = SchemaGenerator::new()
843 .emit_use(false)
844 .emit_model_structs(true);
845 let tables = vec![TableSchema {
846 name: "products".to_string(),
847 columns: vec![
848 ColumnSchema {
849 name: "id".to_string(),
850 rust_type: "i64".to_string(),
851 },
852 ColumnSchema {
853 name: "price".to_string(),
854 rust_type: "Option<f64>".to_string(),
855 },
856 ColumnSchema {
857 name: "description".to_string(),
858 rust_type: "Option<String>".to_string(),
859 },
860 ],
861 }];
862 let output = gen.generate(&tables);
863
864 assert!(output.contains("pub struct Products {"));
865 assert!(output.contains("pub id: i64,"));
866 assert!(output.contains("pub price: Option<f64>,"));
867 assert!(output.contains("pub description: Option<String>,"));
868 }
869
870 #[test]
871 fn test_emit_model_structs_with_serde_use() {
872 let gen = SchemaGenerator::new()
873 .emit_use(true)
874 .emit_model_structs(true);
875 let tables = vec![TableSchema {
876 name: "users".to_string(),
877 columns: vec![ColumnSchema {
878 name: "id".to_string(),
879 rust_type: "i64".to_string(),
880 }],
881 }];
882 let output = gen.generate(&tables);
883
884 assert!(output.contains("use serde::{Serialize, Deserialize};"));
886 }
887
888 #[test]
889 fn test_emit_model_structs_empty_tables() {
890 let gen = SchemaGenerator::new()
891 .emit_use(false)
892 .emit_model_structs(true);
893 let output = gen.generate(&[]);
894
895 assert!(!output.contains("pub struct"));
897 assert!(output.contains("Auto-generated"));
899 }
900}