Skip to main content

sz_orm_core/
schema_gen.rs

1//! Diesel 风格 schema.rs 自动生成
2//!
3//! 从数据库表元数据生成 `typed_query!` 表声明,配合宏做编译期列名校验。
4//!
5//! # 设计
6//!
7//! Diesel 的 `diesel print-schema` 命令从数据库反向生成 `schema.rs` 文件,
8//! 包含 `table!` 宏声明,让 SQL 列名错误在编译期被捕获。
9//!
10//! 本模块提供等价功能,生成 SZ-ORM 的 `typed_query!` 声明:
11//!
12//! ```ignore
13//! // 生成的 schema.rs 内容
14//! use sz_orm_core::typed_query;
15//!
16//! typed_query! {
17//!     table users {
18//!         id: i64,
19//!         name: String,
20//!         email: String,
21//!         created_at: String,
22//!     }
23//! }
24//!
25//! typed_query! {
26//!     table orders {
27//!         order_id: i64,
28//!         user_id: i64,
29//!         total: f64,
30//!     }
31//! }
32//! ```
33
34use std::fmt::Write as _;
35
36/// 单列的元数据(足够生成 typed_query! 声明)
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ColumnSchema {
39    /// 列名
40    pub name: String,
41    /// Rust 类型名(如 "i64"、"String"、"f64"、"`Option<String>`")
42    pub rust_type: String,
43}
44
45/// 单表的元数据
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct TableSchema {
48    /// 表名
49    pub name: String,
50    /// 所有列
51    pub columns: Vec<ColumnSchema>,
52}
53
54/// 生成器:把表元数据列表转换成 schema.rs 文件内容
55pub struct SchemaGenerator {
56    /// 文件头注释(默认包含 SZ-ORM 自动生成提示)
57    header: String,
58    /// 是否生成 `use` 语句
59    emit_use: bool,
60    /// #41 修复:是否同时生成 Model struct 定义
61    ///
62    /// 启用后,会在 typed_query! 声明之前生成对应的 Rust struct 定义,
63    /// 便于用户直接作为 ORM 模型使用,避免手写重复代码。
64    emit_model_structs: bool,
65    /// #41 修复:生成的 Model struct 的派生属性
66    ///
67    /// 默认派生 `Debug, Clone, serde::Serialize, serde::Deserialize`,
68    /// 用户可通过 [`with_model_derives`](SchemaGenerator::with_model_derives) 自定义。
69    model_derives: String,
70}
71
72impl Default for SchemaGenerator {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl SchemaGenerator {
79    /// 创建新的生成器
80    pub fn new() -> Self {
81        Self {
82            header: format!(
83                "// Auto-generated by sz-orm-cli generate schema at {}\n\
84                 // DO NOT EDIT MANUALLY — re-run the command to refresh.\n\
85                 //\n\
86                 // This file contains typed_query! table declarations\n\
87                 // enabling compile-time column-name verification.\n",
88                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
89            ),
90            emit_use: true,
91            emit_model_structs: false,
92            model_derives: "Debug, Clone, serde::Serialize, serde::Deserialize".to_string(),
93        }
94    }
95
96    /// 自定义文件头注释
97    pub fn with_header(mut self, header: impl Into<String>) -> Self {
98        self.header = header.into();
99        self
100    }
101
102    /// 是否生成 `use sz_orm_core::typed_query;` 语句
103    pub fn emit_use(mut self, emit: bool) -> Self {
104        self.emit_use = emit;
105        self
106    }
107
108    /// #41 修复:是否同时生成 Model struct 定义
109    ///
110    /// 启用后,[`generate`](Self::generate) 会在 typed_query! 声明之前
111    /// 生成对应的 Rust struct 定义,便于直接作为 ORM 模型使用。
112    ///
113    /// # 示例
114    ///
115    /// 对于 `users` 表(包含 `id: i64`, `name: String`),会生成:
116    ///
117    /// ```ignore
118    /// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
119    /// pub struct Users {
120    ///     pub id: i64,
121    ///     pub name: String,
122    /// }
123    /// ```
124    pub fn emit_model_structs(mut self, emit: bool) -> Self {
125        self.emit_model_structs = emit;
126        self
127    }
128
129    /// #41 修复:自定义 Model struct 的派生属性
130    ///
131    /// 默认为 `"Debug, Clone, serde::Serialize, serde::Deserialize"`。
132    /// 传入空字符串则不生成 `#[derive(...)]` 属性。
133    pub fn with_model_derives(mut self, derives: impl Into<String>) -> Self {
134        self.model_derives = derives.into();
135        self
136    }
137
138    /// 生成完整的 schema.rs 文件内容
139    pub fn generate(&self, tables: &[TableSchema]) -> String {
140        let mut out = String::new();
141
142        // 文件头
143        let _ = writeln!(out, "{}", self.header);
144        let _ = writeln!(out);
145
146        // use 语句
147        if self.emit_use {
148            let _ = writeln!(out, "use sz_orm_core::typed_query;");
149            // 启用 Model struct 生成时,补充 serde 派生所需的 use 语句
150            // 合并嵌套 if 为单层 if(clippy::collapsible_if)
151            if self.emit_model_structs
152                && !self.model_derives.is_empty()
153                && self.model_derives.contains("serde::")
154            {
155                let _ = writeln!(out, "use serde::{{Serialize, Deserialize}};");
156            }
157            let _ = writeln!(out);
158        }
159
160        // #41 修复:先生成 Model struct 定义(在 typed_query! 之前)
161        if self.emit_model_structs {
162            for (idx, table) in tables.iter().enumerate() {
163                if idx > 0 {
164                    let _ = writeln!(out);
165                }
166                let _ = write!(out, "{}", self.render_model_struct(table));
167            }
168            // Model struct 与 typed_query! 之间空一行
169            if !tables.is_empty() {
170                let _ = writeln!(out);
171                let _ = writeln!(out);
172            }
173        }
174
175        // 每张表生成一个 typed_query! 声明
176        for (idx, table) in tables.iter().enumerate() {
177            if idx > 0 {
178                let _ = writeln!(out);
179            }
180            let _ = write!(out, "{}", self.render_table(table));
181        }
182
183        out
184    }
185
186    /// 渲染单张表的 typed_query! 声明
187    fn render_table(&self, table: &TableSchema) -> String {
188        let mut out = String::new();
189        let _ = writeln!(out, "typed_query! {{");
190        let _ = writeln!(out, "    table {} {{", table.name);
191        for col in &table.columns {
192            let _ = writeln!(out, "        {}: {},", col.name, col.rust_type);
193        }
194        let _ = writeln!(out, "    }}");
195        let _ = writeln!(out, "}}");
196        out
197    }
198
199    /// #41 修复:渲染单张表对应的 Model struct 定义
200    ///
201    /// 将表名转换为 PascalCase 作为 struct 名(如 `users` → `Users`),
202    /// 字段名保持 snake_case(符合 Rust 命名约定)。
203    fn render_model_struct(&self, table: &TableSchema) -> String {
204        let struct_name = to_pascal_case(&table.name);
205        let mut out = String::new();
206        // 派生属性
207        if !self.model_derives.is_empty() {
208            let _ = writeln!(out, "#[derive({})]", self.model_derives);
209        }
210        // 表名注解(便于 ORM 框架映射到具体表)
211        let _ = writeln!(out, "#[table_name = \"{}\"]", table.name);
212        let _ = writeln!(out, "pub struct {} {{", struct_name);
213        for col in &table.columns {
214            let _ = writeln!(out, "    pub {}: {},", col.name, col.rust_type);
215        }
216        let _ = writeln!(out, "}}");
217        out
218    }
219}
220
221/// #41 修复:将 snake_case 或 kebab-case 字符串转换为 PascalCase
222///
223/// 例如:`users` → `Users`,`order_items` → `OrderItems`,
224/// `user-profile` → `UserProfile`。
225fn to_pascal_case(input: &str) -> String {
226    let mut result = String::with_capacity(input.len());
227    let mut next_upper = true;
228    for ch in input.chars() {
229        if ch == '_' || ch == '-' || ch == ' ' {
230            next_upper = true;
231            continue;
232        }
233        if next_upper {
234            result.push(ch.to_ascii_uppercase());
235            next_upper = false;
236        } else {
237            result.push(ch);
238        }
239    }
240    result
241}
242
243/// 把 SQL 类型字符串映射到 Rust 类型字符串
244///
245/// 用于 `generate schema` 命令从 DB 元数据生成 typed_query! 声明
246///
247/// # P0 修复:精确匹配
248///
249/// 旧实现使用 `String::contains` 进行子串匹配,存在以下误判:
250/// - `INTERVAL` 含 `int` 子串 → 误判为 `i32`(应为 `String`)
251/// - `TIMESTAMP` 含 `time` 子串 → 虽因顺序问题未触发,但逻辑脆弱
252/// - `MEDIUMINT` 含 `int` → 正确但依赖匹配顺序,不可靠
253///
254/// 新实现:
255/// 1. 小写化
256/// 2. 剥离参数列表(`DECIMAL(10,2)` → `decimal`、`VARCHAR(255)` → `varchar`)
257/// 3. 剥离无符号/零填充后缀(`INT UNSIGNED` → `int`、`INT ZEROFILL` → `int`)
258/// 4. 精确匹配基础类型名,杜绝子串误判
259pub fn sql_type_to_rust(sql_type: &str, nullable: bool) -> String {
260    // 1. 小写化
261    let lower = sql_type.to_lowercase();
262    // 2. 剥离参数列表: "DECIMAL(10,2)" → "decimal", "VARCHAR(255)" → "varchar"
263    let after_param_strip = lower.split('(').next().unwrap_or(&lower).trim();
264    // 3. 剥离无符号/零填充后缀: "INT UNSIGNED" → "INT", "INT ZEROFILL" → "INT"
265    //    仅剥离 " unsigned" 与 " zerofill" 后缀,不影响其他类型
266    let base_type = after_param_strip
267        .strip_suffix(" unsigned")
268        .or_else(|| after_param_strip.strip_suffix(" zerofill"))
269        .unwrap_or(after_param_strip)
270        .trim();
271
272    let rust = match base_type {
273        // 整数 — 精确匹配,避免 "interval" 误匹配 "int"
274        "tinyint" => "i8",
275        "smallint" | "int2" | "smallserial" => "i16",
276        "bigint" | "int8" | "bigserial" => "i64",
277        "int" | "integer" | "int4" | "mediumint" | "serial" => "i32",
278        // 浮点 — "double precision" 需在 "double" 之前匹配(match 按字面顺序,已显式列出)
279        "float8" | "double" | "double precision" => "f64",
280        "float4" | "real" | "float" => "f32",
281        "decimal" | "numeric" => "f64",
282        // 布尔
283        "bool" | "boolean" => "bool",
284        // 字节
285        "blob" | "bytea" | "binary" | "varbinary" | "tinyblob" | "mediumblob" | "longblob" => {
286            "Vec<u8>"
287        }
288        // 时间 — interval 单独匹配,不再被 int 子串误判
289        "date" => "String",
290        "datetime" | "timestamp" | "timestamptz" => "String",
291        "time" | "timetz" => "String",
292        "interval" => "String",
293        // JSON
294        "json" | "jsonb" => "String",
295        // UUID
296        "uuid" => "String",
297        // 字符串
298        "char" | "varchar" | "text" | "tinytext" | "mediumtext" | "longtext" => "String",
299        "enum" | "set" => "String",
300        // 默认
301        _ => "String",
302    };
303
304    if nullable {
305        format!("Option<{}>", rust)
306    } else {
307        rust.to_string()
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_sql_type_to_rust_int() {
317        assert_eq!(sql_type_to_rust("INT", false), "i32");
318        assert_eq!(sql_type_to_rust("BIGINT", false), "i64");
319        assert_eq!(sql_type_to_rust("SMALLINT", false), "i16");
320        assert_eq!(sql_type_to_rust("TINYINT", false), "i8");
321    }
322
323    #[test]
324    fn test_sql_type_to_rust_float() {
325        assert_eq!(sql_type_to_rust("FLOAT", false), "f32");
326        assert_eq!(sql_type_to_rust("DOUBLE", false), "f64");
327        assert_eq!(sql_type_to_rust("DECIMAL(10,2)", false), "f64");
328    }
329
330    #[test]
331    fn test_sql_type_to_rust_bool() {
332        assert_eq!(sql_type_to_rust("BOOLEAN", false), "bool");
333        assert_eq!(sql_type_to_rust("TINYINT(1)", false), "i8");
334    }
335
336    #[test]
337    fn test_sql_type_to_rust_string() {
338        assert_eq!(sql_type_to_rust("VARCHAR(255)", false), "String");
339        assert_eq!(sql_type_to_rust("TEXT", false), "String");
340        assert_eq!(sql_type_to_rust("CHAR(36)", false), "String");
341    }
342
343    #[test]
344    fn test_sql_type_to_rust_binary() {
345        assert_eq!(sql_type_to_rust("BLOB", false), "Vec<u8>");
346        assert_eq!(sql_type_to_rust("BYTEA", false), "Vec<u8>");
347    }
348
349    #[test]
350    fn test_sql_type_to_rust_nullable() {
351        assert_eq!(sql_type_to_rust("INT", true), "Option<i32>");
352        assert_eq!(sql_type_to_rust("VARCHAR(255)", true), "Option<String>");
353    }
354
355    #[test]
356    fn test_sql_type_to_rust_pg_types() {
357        assert_eq!(sql_type_to_rust("int8", false), "i64");
358        assert_eq!(sql_type_to_rust("int2", false), "i16");
359        assert_eq!(sql_type_to_rust("float8", false), "f64");
360        assert_eq!(sql_type_to_rust("float4", false), "f32");
361        assert_eq!(sql_type_to_rust("bytea", false), "Vec<u8>");
362    }
363
364    #[test]
365    fn test_sql_type_to_rust_json_uuid() {
366        assert_eq!(sql_type_to_rust("JSON", false), "String");
367        assert_eq!(sql_type_to_rust("JSONB", false), "String");
368        assert_eq!(sql_type_to_rust("UUID", false), "String");
369    }
370
371    #[test]
372    fn test_schema_generator_single_table() {
373        let gen = SchemaGenerator::new().emit_use(false);
374        let tables = vec![TableSchema {
375            name: "users".to_string(),
376            columns: vec![
377                ColumnSchema {
378                    name: "id".to_string(),
379                    rust_type: "i64".to_string(),
380                },
381                ColumnSchema {
382                    name: "name".to_string(),
383                    rust_type: "String".to_string(),
384                },
385            ],
386        }];
387        let output = gen.generate(&tables);
388
389        assert!(output.contains("table users {"));
390        assert!(output.contains("id: i64,"));
391        assert!(output.contains("name: String,"));
392        assert!(output.contains("typed_query! {"));
393    }
394
395    #[test]
396    fn test_schema_generator_multiple_tables() {
397        let gen = SchemaGenerator::new().emit_use(false);
398        let tables = vec![
399            TableSchema {
400                name: "users".to_string(),
401                columns: vec![ColumnSchema {
402                    name: "id".to_string(),
403                    rust_type: "i64".to_string(),
404                }],
405            },
406            TableSchema {
407                name: "orders".to_string(),
408                columns: vec![ColumnSchema {
409                    name: "order_id".to_string(),
410                    rust_type: "i64".to_string(),
411                }],
412            },
413        ];
414        let output = gen.generate(&tables);
415
416        assert!(output.contains("table users {"));
417        assert!(output.contains("table orders {"));
418        // 两个表声明之间应有空行
419        let users_end = output.find("}").unwrap();
420        let orders_start = output.find("table orders").unwrap();
421        let between = &output[users_end..orders_start];
422        assert!(between.contains("\n\n"));
423    }
424
425    #[test]
426    fn test_schema_generator_with_use_statement() {
427        let gen = SchemaGenerator::new().emit_use(true);
428        let tables = vec![TableSchema {
429            name: "t".to_string(),
430            columns: vec![],
431        }];
432        let output = gen.generate(&tables);
433
434        assert!(output.contains("use sz_orm_core::typed_query;"));
435    }
436
437    #[test]
438    fn test_schema_generator_header() {
439        let gen = SchemaGenerator::new();
440        let output = gen.generate(&[]);
441        assert!(output.contains("Auto-generated"));
442        assert!(output.contains("DO NOT EDIT MANUALLY"));
443    }
444
445    #[test]
446    fn test_schema_generator_custom_header() {
447        let gen = SchemaGenerator::new().with_header("// Custom header\n");
448        let output = gen.generate(&[]);
449        assert!(output.starts_with("// Custom header"));
450        assert!(!output.contains("Auto-generated"));
451    }
452
453    #[test]
454    fn test_schema_generator_empty_tables() {
455        let gen = SchemaGenerator::new().emit_use(false);
456        let output = gen.generate(&[]);
457        // 空表列表也应生成(仅有 header,不应有 typed_query! 块)
458        assert!(output.contains("Auto-generated"));
459        // 应该没有 typed_query! 块(header 里只是描述性文字,不算块)
460        // 通过检查 "typed_query! {" 来判断是否有实际声明
461        assert!(!output.contains("typed_query! {"));
462    }
463
464    #[test]
465    fn test_schema_generator_option_type() {
466        let gen = SchemaGenerator::new().emit_use(false);
467        let tables = vec![TableSchema {
468            name: "products".to_string(),
469            columns: vec![ColumnSchema {
470                name: "price".to_string(),
471                rust_type: "Option<f64>".to_string(),
472            }],
473        }];
474        let output = gen.generate(&tables);
475
476        assert!(output.contains("price: Option<f64>,"));
477    }
478
479    #[test]
480    fn test_schema_generator_compound_type() {
481        let gen = SchemaGenerator::new().emit_use(false);
482        let tables = vec![TableSchema {
483            name: "files".to_string(),
484            columns: vec![ColumnSchema {
485                name: "content".to_string(),
486                rust_type: "Vec<u8>".to_string(),
487            }],
488        }];
489        let output = gen.generate(&tables);
490
491        assert!(output.contains("content: Vec<u8>,"));
492    }
493
494    #[test]
495    fn test_generated_code_is_valid_syntax() {
496        // 验证生成的代码包含正确的 typed_query! 调用语法
497        let gen = SchemaGenerator::new().emit_use(false);
498        let tables = vec![TableSchema {
499            name: "typed_validate_test".to_string(),
500            columns: vec![
501                ColumnSchema {
502                    name: "id".to_string(),
503                    rust_type: "i64".to_string(),
504                },
505                ColumnSchema {
506                    name: "name".to_string(),
507                    rust_type: "String".to_string(),
508                },
509            ],
510        }];
511        let output = gen.generate(&tables);
512
513        // 验证生成的代码包含 typed_query! 块的完整结构
514        assert!(output.contains("typed_query! {"));
515        assert!(output.contains("table typed_validate_test {"));
516        assert!(output.contains("id: i64,"));
517        assert!(output.contains("name: String,"));
518        // 块应正确闭合
519        let count_open = output.matches("typed_query! {").count();
520        let count_close = output.matches("}\n}").count();
521        assert_eq!(count_open, 1);
522        assert_eq!(count_close, 1);
523    }
524
525    // ---- P0 修复:精确匹配测试(杜绝 contains 子串误判) ----
526
527    #[test]
528    fn test_sql_type_to_rust_interval_not_int() {
529        // 关键修复:INTERVAL 不应被 contains("int") 误判为 i32
530        assert_eq!(sql_type_to_rust("INTERVAL", false), "String");
531        assert_eq!(sql_type_to_rust("interval", false), "String");
532        // PostgreSQL INTERVAL DAY TO SECOND 等变体
533        assert_eq!(sql_type_to_rust("INTERVAL DAY TO SECOND", false), "String");
534    }
535
536    #[test]
537    fn test_sql_type_to_rust_mediumint() {
538        // MySQL MEDIUMINT 应为 i32
539        assert_eq!(sql_type_to_rust("MEDIUMINT", false), "i32");
540        assert_eq!(sql_type_to_rust("MEDIUMINT(8)", false), "i32");
541    }
542
543    #[test]
544    fn test_sql_type_to_rust_integer_alias() {
545        // INTEGER 是 INT 的别名
546        assert_eq!(sql_type_to_rust("INTEGER", false), "i32");
547        assert_eq!(sql_type_to_rust("INTEGER(11)", false), "i32");
548    }
549
550    #[test]
551    fn test_sql_type_to_rust_serial_types() {
552        // PostgreSQL SERIAL/BIGSERIAL/SMALLSERIAL
553        assert_eq!(sql_type_to_rust("SERIAL", false), "i32");
554        assert_eq!(sql_type_to_rust("BIGSERIAL", false), "i64");
555        assert_eq!(sql_type_to_rust("SMALLSERIAL", false), "i16");
556    }
557
558    #[test]
559    fn test_sql_type_to_rust_unsigned_suffix() {
560        // MySQL UNSIGNED 后缀剥离
561        assert_eq!(sql_type_to_rust("INT UNSIGNED", false), "i32");
562        assert_eq!(sql_type_to_rust("BIGINT UNSIGNED", false), "i64");
563        assert_eq!(sql_type_to_rust("TINYINT UNSIGNED", false), "i8");
564        assert_eq!(sql_type_to_rust("SMALLINT UNSIGNED", false), "i16");
565        assert_eq!(sql_type_to_rust("MEDIUMINT UNSIGNED", false), "i32");
566    }
567
568    #[test]
569    fn test_sql_type_to_rust_zerofill_suffix() {
570        // MySQL ZEROFILL 后缀剥离
571        assert_eq!(sql_type_to_rust("INT ZEROFILL", false), "i32");
572        assert_eq!(sql_type_to_rust("INT(4) ZEROFILL", false), "i32");
573    }
574
575    #[test]
576    fn test_sql_type_to_rust_timestamptz() {
577        // PostgreSQL TIMESTAMPTZ
578        assert_eq!(sql_type_to_rust("TIMESTAMPTZ", false), "String");
579        assert_eq!(sql_type_to_rust("timestamptz", false), "String");
580    }
581
582    #[test]
583    fn test_sql_type_to_rust_timetz() {
584        // PostgreSQL TIMETZ
585        assert_eq!(sql_type_to_rust("TIMETZ", false), "String");
586        assert_eq!(sql_type_to_rust("timetz", false), "String");
587    }
588
589    #[test]
590    fn test_sql_type_to_rust_double_precision() {
591        // PostgreSQL DOUBLE PRECISION(带空格)
592        assert_eq!(sql_type_to_rust("DOUBLE PRECISION", false), "f64");
593        assert_eq!(sql_type_to_rust("double precision", false), "f64");
594    }
595
596    #[test]
597    fn test_sql_type_to_rust_varbinary() {
598        // MySQL VARBINARY
599        assert_eq!(sql_type_to_rust("VARBINARY(255)", false), "Vec<u8>");
600        assert_eq!(sql_type_to_rust("VARBINARY", false), "Vec<u8>");
601    }
602
603    #[test]
604    fn test_sql_type_to_rust_blob_variants() {
605        // MySQL TINYBLOB/MEDIUMBLOB/LONGBLOB
606        assert_eq!(sql_type_to_rust("TINYBLOB", false), "Vec<u8>");
607        assert_eq!(sql_type_to_rust("MEDIUMBLOB", false), "Vec<u8>");
608        assert_eq!(sql_type_to_rust("LONGBLOB", false), "Vec<u8>");
609    }
610
611    #[test]
612    fn test_sql_type_to_rust_text_variants() {
613        // MySQL TINYTEXT/MEDIUMTEXT/LONGTEXT
614        assert_eq!(sql_type_to_rust("TINYTEXT", false), "String");
615        assert_eq!(sql_type_to_rust("MEDIUMTEXT", false), "String");
616        assert_eq!(sql_type_to_rust("LONGTEXT", false), "String");
617    }
618
619    #[test]
620    fn test_sql_type_to_rust_enum_and_set() {
621        // MySQL ENUM/SET
622        assert_eq!(sql_type_to_rust("ENUM('a','b')", false), "String");
623        assert_eq!(sql_type_to_rust("SET('a','b')", false), "String");
624    }
625
626    #[test]
627    fn test_sql_type_to_rust_decimal_with_space() {
628        // DECIMAL(10, 2)(参数内含空格)
629        assert_eq!(sql_type_to_rust("DECIMAL(10, 2)", false), "f64");
630        assert_eq!(sql_type_to_rust("NUMERIC(8, 2)", false), "f64");
631    }
632
633    #[test]
634    fn test_sql_type_to_rust_unknown_defaults_string() {
635        // 未知类型默认为 String
636        assert_eq!(sql_type_to_rust("UNKNOWN_TYPE", false), "String");
637        assert_eq!(sql_type_to_rust("citext", false), "String");
638        assert_eq!(sql_type_to_rust("money", false), "String");
639    }
640
641    #[test]
642    fn test_sql_type_to_rust_interval_nullable() {
643        // INTERVAL 可空 → Option<String>
644        assert_eq!(sql_type_to_rust("INTERVAL", true), "Option<String>");
645    }
646
647    // ====================================================================
648    // #41 修复:Model struct 生成测试
649    // ====================================================================
650
651    #[test]
652    fn test_to_pascal_case_basic() {
653        assert_eq!(to_pascal_case("users"), "Users");
654        assert_eq!(to_pascal_case("orders"), "Orders");
655    }
656
657    #[test]
658    fn test_to_pascal_case_snake_case() {
659        assert_eq!(to_pascal_case("order_items"), "OrderItems");
660        assert_eq!(to_pascal_case("user_profiles"), "UserProfiles");
661    }
662
663    #[test]
664    fn test_to_pascal_case_kebab_case() {
665        assert_eq!(to_pascal_case("user-profile"), "UserProfile");
666        assert_eq!(to_pascal_case("order-item"), "OrderItem");
667    }
668
669    #[test]
670    fn test_to_pascal_case_with_spaces() {
671        assert_eq!(to_pascal_case("user profile"), "UserProfile");
672    }
673
674    #[test]
675    fn test_to_pascal_case_single_char() {
676        assert_eq!(to_pascal_case("a"), "A");
677        assert_eq!(to_pascal_case("_a"), "A");
678    }
679
680    #[test]
681    fn test_to_pascal_case_already_pascal() {
682        // 已是 PascalCase 的输入应保持原样(首字母仍大写)
683        assert_eq!(to_pascal_case("Users"), "Users");
684    }
685
686    #[test]
687    fn test_emit_model_structs_generates_struct() {
688        let gen = SchemaGenerator::new()
689            .emit_use(false)
690            .emit_model_structs(true);
691        let tables = vec![TableSchema {
692            name: "users".to_string(),
693            columns: vec![
694                ColumnSchema {
695                    name: "id".to_string(),
696                    rust_type: "i64".to_string(),
697                },
698                ColumnSchema {
699                    name: "name".to_string(),
700                    rust_type: "String".to_string(),
701                },
702            ],
703        }];
704        let output = gen.generate(&tables);
705
706        // 应生成 struct Users
707        assert!(output.contains("pub struct Users {"));
708        assert!(output.contains("pub id: i64,"));
709        assert!(output.contains("pub name: String,"));
710        // 应生成派生属性
711        assert!(output.contains("#[derive("));
712        assert!(output.contains("Debug"));
713        assert!(output.contains("Clone"));
714        // 应生成表名注解
715        assert!(output.contains("#[table_name = \"users\"]"));
716        // 同时应生成 typed_query! 声明
717        assert!(output.contains("typed_query! {"));
718        assert!(output.contains("table users {"));
719    }
720
721    #[test]
722    fn test_emit_model_structs_disabled_by_default() {
723        let gen = SchemaGenerator::new().emit_use(false);
724        let tables = vec![TableSchema {
725            name: "users".to_string(),
726            columns: vec![ColumnSchema {
727                name: "id".to_string(),
728                rust_type: "i64".to_string(),
729            }],
730        }];
731        let output = gen.generate(&tables);
732
733        // 默认不生成 Model struct
734        assert!(!output.contains("pub struct Users"));
735        // 但应生成 typed_query! 声明
736        assert!(output.contains("typed_query! {"));
737    }
738
739    #[test]
740    fn test_emit_model_structs_multiple_tables() {
741        let gen = SchemaGenerator::new()
742            .emit_use(false)
743            .emit_model_structs(true);
744        let tables = vec![
745            TableSchema {
746                name: "users".to_string(),
747                columns: vec![ColumnSchema {
748                    name: "id".to_string(),
749                    rust_type: "i64".to_string(),
750                }],
751            },
752            TableSchema {
753                name: "order_items".to_string(),
754                columns: vec![ColumnSchema {
755                    name: "item_id".to_string(),
756                    rust_type: "i64".to_string(),
757                }],
758            },
759        ];
760        let output = gen.generate(&tables);
761
762        // 应生成两个 struct,且 snake_case 表名转换为 PascalCase
763        assert!(output.contains("pub struct Users {"));
764        assert!(output.contains("pub struct OrderItems {"));
765    }
766
767    #[test]
768    fn test_emit_model_structs_with_custom_derives() {
769        let gen = SchemaGenerator::new()
770            .emit_use(false)
771            .emit_model_structs(true)
772            .with_model_derives("Debug, Clone");
773        let tables = vec![TableSchema {
774            name: "users".to_string(),
775            columns: vec![ColumnSchema {
776                name: "id".to_string(),
777                rust_type: "i64".to_string(),
778            }],
779        }];
780        let output = gen.generate(&tables);
781
782        // 应使用自定义派生属性
783        assert!(output.contains("#[derive(Debug, Clone)]"));
784        // 不应包含默认的 serde 派生
785        assert!(!output.contains("serde::Serialize"));
786    }
787
788    #[test]
789    fn test_emit_model_structs_with_empty_derives() {
790        let gen = SchemaGenerator::new()
791            .emit_use(false)
792            .emit_model_structs(true)
793            .with_model_derives("");
794        let tables = vec![TableSchema {
795            name: "users".to_string(),
796            columns: vec![ColumnSchema {
797                name: "id".to_string(),
798                rust_type: "i64".to_string(),
799            }],
800        }];
801        let output = gen.generate(&tables);
802
803        // 不应生成 #[derive(...)] 属性
804        assert!(!output.contains("#[derive("));
805        // 但仍应生成 struct 定义
806        assert!(output.contains("pub struct Users {"));
807    }
808
809    #[test]
810    fn test_emit_model_structs_with_nullable_fields() {
811        let gen = SchemaGenerator::new()
812            .emit_use(false)
813            .emit_model_structs(true);
814        let tables = vec![TableSchema {
815            name: "products".to_string(),
816            columns: vec![
817                ColumnSchema {
818                    name: "id".to_string(),
819                    rust_type: "i64".to_string(),
820                },
821                ColumnSchema {
822                    name: "price".to_string(),
823                    rust_type: "Option<f64>".to_string(),
824                },
825                ColumnSchema {
826                    name: "description".to_string(),
827                    rust_type: "Option<String>".to_string(),
828                },
829            ],
830        }];
831        let output = gen.generate(&tables);
832
833        assert!(output.contains("pub struct Products {"));
834        assert!(output.contains("pub id: i64,"));
835        assert!(output.contains("pub price: Option<f64>,"));
836        assert!(output.contains("pub description: Option<String>,"));
837    }
838
839    #[test]
840    fn test_emit_model_structs_with_serde_use() {
841        let gen = SchemaGenerator::new()
842            .emit_use(true)
843            .emit_model_structs(true);
844        let tables = vec![TableSchema {
845            name: "users".to_string(),
846            columns: vec![ColumnSchema {
847                name: "id".to_string(),
848                rust_type: "i64".to_string(),
849            }],
850        }];
851        let output = gen.generate(&tables);
852
853        // 启用 serde 派生时应生成对应的 use 语句
854        assert!(output.contains("use serde::{Serialize, Deserialize};"));
855    }
856
857    #[test]
858    fn test_emit_model_structs_empty_tables() {
859        let gen = SchemaGenerator::new()
860            .emit_use(false)
861            .emit_model_structs(true);
862        let output = gen.generate(&[]);
863
864        // 空表列表不应生成任何 struct
865        assert!(!output.contains("pub struct"));
866        // 但应有 header
867        assert!(output.contains("Auto-generated"));
868    }
869}