Skip to main content

sz_orm_migration/
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_migration::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    /// P1-7:生成的 `use` 语句中使用的 crate 前缀。
71    ///
72    /// 默认为 `"sz_orm_core"`。若下游项目直接使用 `sz_orm_query`,
73    /// 可通过 [`with_crate_prefix`](SchemaGenerator::with_crate_prefix) 改为 `"sz_orm_query"`。
74    crate_prefix: String,
75}
76
77impl Default for SchemaGenerator {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl SchemaGenerator {
84    /// 创建新的生成器
85    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    /// P1-7:自定义生成代码中的 crate 前缀。
103    ///
104    /// 默认 `"sz_orm_core"`;若项目直接依赖 `sz_orm_query`,
105    /// 可设为 `"sz_orm_query"` 使生成的 `use` 语句与项目依赖对齐。
106    pub fn with_crate_prefix(mut self, prefix: impl Into<String>) -> Self {
107        self.crate_prefix = prefix.into();
108        self
109    }
110
111    /// 自定义文件头注释
112    pub fn with_header(mut self, header: impl Into<String>) -> Self {
113        self.header = header.into();
114        self
115    }
116
117    /// 是否生成 `use sz_orm_migration::typed_query;` 语句
118    pub fn emit_use(mut self, emit: bool) -> Self {
119        self.emit_use = emit;
120        self
121    }
122
123    /// #41 修复:是否同时生成 Model struct 定义
124    ///
125    /// 启用后,[`generate`](Self::generate) 会在 typed_query! 声明之前
126    /// 生成对应的 Rust struct 定义,便于直接作为 ORM 模型使用。
127    ///
128    /// # 示例
129    ///
130    /// 对于 `users` 表(包含 `id: i64`, `name: String`),会生成:
131    ///
132    /// ```ignore
133    /// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
134    /// pub struct Users {
135    ///     pub id: i64,
136    ///     pub name: String,
137    /// }
138    /// ```
139    pub fn emit_model_structs(mut self, emit: bool) -> Self {
140        self.emit_model_structs = emit;
141        self
142    }
143
144    /// #41 修复:自定义 Model struct 的派生属性
145    ///
146    /// 默认为 `"Debug, Clone, serde::Serialize, serde::Deserialize"`。
147    /// 传入空字符串则不生成 `#[derive(...)]` 属性。
148    pub fn with_model_derives(mut self, derives: impl Into<String>) -> Self {
149        self.model_derives = derives.into();
150        self
151    }
152
153    /// 生成完整的 schema.rs 文件内容
154    pub fn generate(&self, tables: &[TableSchema]) -> String {
155        let mut out = String::new();
156
157        // 文件头
158        let _ = writeln!(out, "{}", self.header);
159        let _ = writeln!(out);
160
161        // use 语句
162        if self.emit_use {
163            let _ = writeln!(out, "use {}::typed_query;", self.crate_prefix);
164            // 启用 Model struct 生成时,补充 serde 派生所需的 use 语句
165            // 合并嵌套 if 为单层 if(clippy::collapsible_if)
166            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        // #41 修复:先生成 Model struct 定义(在 typed_query! 之前)
176        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            // Model struct 与 typed_query! 之间空一行
184            if !tables.is_empty() {
185                let _ = writeln!(out);
186                let _ = writeln!(out);
187            }
188        }
189
190        // 每张表生成一个 typed_query! 声明
191        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    /// 渲染单张表的 typed_query! 声明
202    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    /// #41 修复:渲染单张表对应的 Model struct 定义
215    ///
216    /// 将表名转换为 PascalCase 作为 struct 名(如 `users` → `Users`),
217    /// 字段名保持 snake_case(符合 Rust 命名约定)。
218    fn render_model_struct(&self, table: &TableSchema) -> String {
219        let struct_name = to_pascal_case(&table.name);
220        let mut out = String::new();
221        // 派生属性
222        if !self.model_derives.is_empty() {
223            let _ = writeln!(out, "#[derive({})]", self.model_derives);
224        }
225        // 表名注解(便于 ORM 框架映射到具体表)
226        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
236/// #41 修复:将 snake_case 或 kebab-case 字符串转换为 PascalCase
237///
238/// 例如:`users` → `Users`,`order_items` → `OrderItems`,
239/// `user-profile` → `UserProfile`。
240fn 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
258/// 把 SQL 类型字符串映射到 Rust 类型字符串
259///
260/// 用于 `generate schema` 命令从 DB 元数据生成 typed_query! 声明
261///
262/// # P0 修复:精确匹配
263///
264/// 旧实现使用 `String::contains` 进行子串匹配,存在以下误判:
265/// - `INTERVAL` 含 `int` 子串 → 误判为 `i32`(应为 `String`)
266/// - `TIMESTAMP` 含 `time` 子串 → 虽因顺序问题未触发,但逻辑脆弱
267/// - `MEDIUMINT` 含 `int` → 正确但依赖匹配顺序,不可靠
268///
269/// 新实现:
270/// 1. 小写化
271/// 2. 剥离参数列表(`DECIMAL(10,2)` → `decimal`、`VARCHAR(255)` → `varchar`)
272/// 3. 剥离无符号/零填充后缀(`INT UNSIGNED` → `int`、`INT ZEROFILL` → `int`)
273/// 4. 精确匹配基础类型名,杜绝子串误判
274pub fn sql_type_to_rust(sql_type: &str, nullable: bool) -> String {
275    // 1. 小写化
276    let lower = sql_type.to_lowercase();
277    // 2. 剥离参数列表: "DECIMAL(10,2)" → "decimal", "VARCHAR(255)" → "varchar"
278    let after_param_strip = lower.split('(').next().unwrap_or(&lower).trim();
279    // 3. 剥离无符号/零填充后缀: "INT UNSIGNED" → "INT", "INT ZEROFILL" → "INT"
280    //    仅剥离 " unsigned" 与 " zerofill" 后缀,不影响其他类型
281    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        // 整数 — 精确匹配,避免 "interval" 误匹配 "int"
289        "tinyint" => "i8",
290        "smallint" | "int2" | "smallserial" => "i16",
291        "bigint" | "int8" | "bigserial" => "i64",
292        "int" | "integer" | "int4" | "mediumint" | "serial" => "i32",
293        // 浮点 — "double precision" 需在 "double" 之前匹配(match 按字面顺序,已显式列出)
294        "float8" | "double" | "double precision" => "f64",
295        "float4" | "real" | "float" => "f32",
296        "decimal" | "numeric" => "f64",
297        // 布尔
298        "bool" | "boolean" => "bool",
299        // 字节
300        "blob" | "bytea" | "binary" | "varbinary" | "tinyblob" | "mediumblob" | "longblob" => {
301            "Vec<u8>"
302        }
303        // 时间 — interval 单独匹配,不再被 int 子串误判
304        "date" => "String",
305        "datetime" | "timestamp" | "timestamptz" => "String",
306        "time" | "timetz" => "String",
307        "interval" => "String",
308        // JSON
309        "json" | "jsonb" => "String",
310        // UUID
311        "uuid" => "String",
312        // 字符串
313        "char" | "varchar" | "text" | "tinytext" | "mediumtext" | "longtext" => "String",
314        "enum" | "set" => "String",
315        // 默认
316        _ => "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        // 两个表声明之间应有空行
434        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    /// P1-7:自定义 crate 前缀应反映在生成的 `use` 语句中。
453    #[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        // 空表列表也应生成(仅有 header,不应有 typed_query! 块)
489        assert!(output.contains("Auto-generated"));
490        // 应该没有 typed_query! 块(header 里只是描述性文字,不算块)
491        // 通过检查 "typed_query! {" 来判断是否有实际声明
492        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        // 验证生成的代码包含正确的 typed_query! 调用语法
528        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        // 验证生成的代码包含 typed_query! 块的完整结构
545        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        // 块应正确闭合
550        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    // ---- P0 修复:精确匹配测试(杜绝 contains 子串误判) ----
557
558    #[test]
559    fn test_sql_type_to_rust_interval_not_int() {
560        // 关键修复:INTERVAL 不应被 contains("int") 误判为 i32
561        assert_eq!(sql_type_to_rust("INTERVAL", false), "String");
562        assert_eq!(sql_type_to_rust("interval", false), "String");
563        // PostgreSQL INTERVAL DAY TO SECOND 等变体
564        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        // MySQL MEDIUMINT 应为 i32
570        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        // INTEGER 是 INT 的别名
577        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        // PostgreSQL SERIAL/BIGSERIAL/SMALLSERIAL
584        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        // MySQL UNSIGNED 后缀剥离
592        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        // MySQL ZEROFILL 后缀剥离
602        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        // PostgreSQL TIMESTAMPTZ
609        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        // PostgreSQL TIMETZ
616        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        // PostgreSQL DOUBLE PRECISION(带空格)
623        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        // MySQL VARBINARY
630        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        // MySQL TINYBLOB/MEDIUMBLOB/LONGBLOB
637        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        // MySQL TINYTEXT/MEDIUMTEXT/LONGTEXT
645        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        // MySQL ENUM/SET
653        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        // DECIMAL(10, 2)(参数内含空格)
660        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        // 未知类型默认为 String
667        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        // INTERVAL 可空 → Option<String>
675        assert_eq!(sql_type_to_rust("INTERVAL", true), "Option<String>");
676    }
677
678    // ====================================================================
679    // #41 修复:Model struct 生成测试
680    // ====================================================================
681
682    #[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        // 已是 PascalCase 的输入应保持原样(首字母仍大写)
714        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        // 应生成 struct Users
738        assert!(output.contains("pub struct Users {"));
739        assert!(output.contains("pub id: i64,"));
740        assert!(output.contains("pub name: String,"));
741        // 应生成派生属性
742        assert!(output.contains("#[derive("));
743        assert!(output.contains("Debug"));
744        assert!(output.contains("Clone"));
745        // 应生成表名注解
746        assert!(output.contains("#[table_name = \"users\"]"));
747        // 同时应生成 typed_query! 声明
748        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        // 默认不生成 Model struct
765        assert!(!output.contains("pub struct Users"));
766        // 但应生成 typed_query! 声明
767        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        // 应生成两个 struct,且 snake_case 表名转换为 PascalCase
794        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        // 应使用自定义派生属性
814        assert!(output.contains("#[derive(Debug, Clone)]"));
815        // 不应包含默认的 serde 派生
816        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        // 不应生成 #[derive(...)] 属性
835        assert!(!output.contains("#[derive("));
836        // 但仍应生成 struct 定义
837        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        // 启用 serde 派生时应生成对应的 use 语句
885        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        // 空表列表不应生成任何 struct
896        assert!(!output.contains("pub struct"));
897        // 但应有 header
898        assert!(output.contains("Auto-generated"));
899    }
900}