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