Skip to main content

sz_orm_macros/
lib.rs

1//! SZ-ORM Procedural Macros - compile-time SQL validation & derive macros
2//!
3//! Provides:
4//! - `sql_string!` macro that validates SQL string literals at compile time.
5//!   Errors like `SELECT * FORM users` or `'; DROP TABLE` are caught before the binary is built.
6//! - `#[derive(Schema)]` — auto-generate table structure info from a struct.
7//! - `#[derive(Builder)]` — auto-generate builder pattern code for a struct.
8//!
9//! # Usage
10//!
11//! ```ignore
12//! use sz_orm_macros::sql_string;
13//!
14//! // Basic usage
15//! let sql = sql_string!("SELECT * FROM users WHERE id = 1"); // ✅ compiles
16//!
17//! // With parameter count check
18//! let sql = sql_string!("SELECT * FROM users WHERE id = ?");
19//!                      params: 1);                          // ✅ compiles
20//!
21//! // ❌ compile error: missing FROM
22//! let sql = sql_string!("SELECT * users WHERE id = 1");
23//!
24//! // ❌ compile error: SQL injection detected
25//! let sql = sql_string!("SELECT * FROM users WHERE name = 'x' OR '1'='1'");
26//!
27//! // ❌ compile error: parameter count mismatch
28//! let sql = sql_string!("SELECT * FROM users WHERE id = ?");
29//!                      params: 2);
30//! ```
31
32// 抑制 Windows 链接器输出"正在创建库 ..."的诊断信息被识别为警告:
33// 该输出是 link.exe 创建 DLL 导入库时的正常 stdout 提示,并非代码问题。
34#![allow(linker_messages)]
35//!
36//! # Derive macros
37//!
38//! ```ignore
39//! use sz_orm_macros::{Schema, Builder};
40//!
41//! #[derive(Schema)]
42//! #[table(name = "users")]
43//! struct User {
44//!     #[column(primary_key)]
45//!     id: i64,
46//!     name: String,
47//! }
48//!
49//! #[derive(Builder)]
50//! struct Order {
51//!     id: i64,
52//!     total: f64,
53//! }
54//! ```
55
56extern crate proc_macro;
57
58use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
59
60#[cfg(feature = "db-verify")]
61use sqlx::Row as _;
62
63// 引入 quote! 宏,用于类型安全地构建 TokenStream
64use proc_macro2::TokenStream as TokenStream2;
65use quote::quote;
66use syn::parse_macro_input;
67
68// 派生宏模块
69mod derive;
70
71// v3.9.0:数据验证派生宏模块(data-validation feature 隔离)
72#[cfg(feature = "data-validation")]
73mod derive_validate;
74
75// 自定义编译期诊断模块(typed-dsl 或 custom-diagnostic feature 隔离)
76#[cfg(any(feature = "typed-dsl", feature = "custom-diagnostic"))]
77mod diagnostic;
78
79// ---- 自定义编译期诊断宏(typed-dsl 或 custom-diagnostic feature 隔离)----
80
81/// `#[type_check]` attribute macro — adds type-check diagnostic hints to a function
82///
83/// When a type constraint inside the function body fails, generates clearer
84/// diagnostic messages than Rust's defaults.
85///
86/// # Example
87///
88/// ```ignore
89/// #[type_check]
90/// fn my_query() {
91///     let expr = ColId.eq("hello"); // i64 column compared with String
92///     // Compile-time will generate custom diagnostic information
93/// }
94/// ```
95#[cfg(any(feature = "typed-dsl", feature = "custom-diagnostic"))]
96#[proc_macro_attribute]
97pub fn type_check(_attr: TokenStream, item: TokenStream) -> TokenStream {
98    let input_fn = parse_macro_input!(item as syn::ItemFn);
99    let fn_name = input_fn.sig.ident.to_string();
100    let fn_vis = &input_fn.vis;
101    let fn_sig = &input_fn.sig;
102    let fn_block = &input_fn.block;
103
104    let expanded = quote! {
105        #[doc = concat!("Type-check function `", #fn_name, "`: if compilation fails, please verify type constraints")]
106        #fn_vis #fn_sig {
107            #fn_block
108        }
109    };
110
111    TokenStream::from(expanded)
112}
113
114/// `diagnostic_error!` macro — generates a compile-time error with suggestion information
115///
116/// # Example
117///
118/// ```ignore
119/// diagnostic_error!("type mismatch", "please use Cast for explicit conversion");
120/// ```
121#[cfg(any(feature = "typed-dsl", feature = "custom-diagnostic"))]
122#[proc_macro]
123pub fn diagnostic_error(input: TokenStream) -> TokenStream {
124    let mut iter = input.into_iter().peekable();
125
126    let msg = match iter.next() {
127        Some(TokenTree::Literal(l)) => l.to_string(),
128        _ => {
129            return "compile_error!(\"diagnostic_error! requires string literal arguments\")"
130                .parse()
131                .unwrap()
132        }
133    };
134
135    let suggestion: Option<String> = match iter.next() {
136        Some(TokenTree::Punct(p)) if p.as_char() == ',' => match iter.next() {
137            Some(TokenTree::Literal(l)) => Some(l.to_string()),
138            _ => None,
139        },
140        _ => None,
141    };
142
143    let stripped_msg = diagnostic::strip_quotes(&msg);
144    let full_msg = if let Some(sug) = suggestion.as_ref() {
145        let stripped_sug = diagnostic::strip_quotes(sug);
146        format!("{}\n  help: {}", stripped_msg, stripped_sug)
147    } else {
148        stripped_msg.to_string()
149    };
150
151    let err = format!("compile_error!({:?})", full_msg);
152    err.parse().unwrap_or_else(|_| {
153        "compile_error!(\"internal error in diagnostic macro\")"
154            .parse()
155            .unwrap()
156    })
157}
158
159/// Compile-time SQL validation macro.
160///
161/// Validates SQL syntax at compile time and emits the validated SQL string.
162///
163/// # Syntax
164///
165/// - `sql_string!("SQL")` — validates the SQL and emits it as a `&str`
166/// - `sql_string!("SQL"; params: N)` — additionally checks that the SQL has exactly N parameters
167///
168/// # Validation rules
169///
170/// - SELECT must contain FROM
171/// - INSERT must contain INTO and VALUES
172/// - UPDATE must contain SET
173/// - DELETE must contain FROM
174/// - Parentheses must be balanced
175/// - String literals must be properly closed
176/// - No SQL injection patterns (OR '1'='1', UNION SELECT, `'; DROP TABLE`, `--`, `/*`)
177/// - Table/column identifiers must be valid
178#[proc_macro]
179pub fn sql_string(input: TokenStream) -> TokenStream {
180    let mut tokens = input.into_iter().peekable();
181
182    // Parse the SQL string literal
183    let sql = match tokens.next() {
184        Some(TokenTree::Literal(lit)) => lit.to_string(),
185        Some(other) => {
186            return compile_error(
187                other.span(),
188                "Expected a string literal as the first argument to sql_string!",
189            );
190        }
191        None => {
192            return compile_error(
193                Span::call_site(),
194                "Expected a string literal argument to sql_string!",
195            );
196        }
197    };
198
199    // Remove surrounding quotes from the string literal
200    let sql_content = if sql.starts_with("r#\"") {
201        &sql[3..sql.len() - 2]
202    } else if sql.starts_with("r\"") {
203        &sql[2..sql.len() - 1]
204    } else if sql.starts_with('"') {
205        &sql[1..sql.len() - 1]
206    } else if sql.starts_with("b\"") || sql.starts_with("b\'") {
207        &sql[2..sql.len() - 1]
208    } else {
209        return compile_error(
210            Span::call_site(),
211            "sql_string! requires a string literal argument",
212        );
213    };
214
215    // Parse optional `params: N`
216    let mut expected_params = None;
217    if tokens.peek().is_some() {
218        // Expect `; params: N`
219        match tokens.next() {
220            Some(TokenTree::Punct(p)) if p.as_char() == ';' => {}
221            Some(other) => {
222                return compile_error(
223                    other.span(),
224                    "Expected `;` before param count, e.g. sql_string!(\"...\"; params: 2)",
225                );
226            }
227            None => {}
228        }
229
230        // Parse `params`
231        match tokens.next() {
232            Some(TokenTree::Ident(id)) if id.to_string() == "params" => {}
233            Some(other) => {
234                return compile_error(
235                    other.span(),
236                    "Expected `params:` keyword, e.g. sql_string!(\"...\"; params: 2)",
237                );
238            }
239            None => {
240                return compile_error(Span::call_site(), "Expected param count after `;`");
241            }
242        }
243
244        // Parse `:`
245        match tokens.next() {
246            Some(TokenTree::Punct(p)) if p.as_char() == ':' => {}
247            Some(other) => {
248                return compile_error(
249                    other.span(),
250                    "Expected `:` after `params`, e.g. sql_string!(\"...\"; params: 2)",
251                );
252            }
253            None => {
254                return compile_error(Span::call_site(), "Expected param count after `params`");
255            }
256        }
257
258        // Parse the number
259        match tokens.next() {
260            Some(TokenTree::Literal(lit)) => {
261                let num_str = lit.to_string();
262                if let Ok(n) = num_str.parse::<usize>() {
263                    expected_params = Some(n);
264                } else {
265                    return compile_error(
266                        lit.span(),
267                        "Expected a positive integer for param count",
268                    );
269                }
270            }
271            Some(other) => {
272                return compile_error(
273                    other.span(),
274                    "Expected a number after `params:`, e.g. sql_string!(\"...\"; params: 2)",
275                );
276            }
277            None => {
278                return compile_error(Span::call_site(), "Expected a number after `params:`");
279            }
280        }
281    }
282
283    // Run validation
284    if let Err(err_msg) = validate_sql_content(sql_content, expected_params) {
285        return compile_error(Span::call_site(), &err_msg);
286    }
287
288    // Emit the validated string as a &str literal
289    let output = format!("\"{}\"", sql_content.escape_default());
290    output
291        .parse()
292        .unwrap_or_else(|_| compile_error(Span::call_site(), "Failed to generate output token"))
293}
294
295// ---------------------------------------------------------------------------
296// Validation logic (self-contained, no external dependencies)
297// ---------------------------------------------------------------------------
298
299fn validate_sql_content(sql: &str, expected_params: Option<usize>) -> Result<(), String> {
300    let trimmed = sql.trim();
301    if trimmed.is_empty() {
302        return Err("SQL statement is empty".to_string());
303    }
304
305    validate_balanced_parens(trimmed)?;
306    validate_string_literals_closed(trimmed)?;
307    validate_no_injection(trimmed)?;
308
309    // Type-specific validation
310    let sql_upper = trimmed.to_uppercase();
311    if sql_upper.starts_with("SELECT") {
312        if !sql_upper.contains("FROM") {
313            return Err("SELECT statement missing FROM clause".to_string());
314        }
315    } else if sql_upper.starts_with("INSERT") {
316        if !sql_upper.contains("INTO") {
317            return Err("INSERT statement missing INTO clause".to_string());
318        }
319        if !sql_upper.contains("VALUES") {
320            return Err("INSERT statement missing VALUES clause".to_string());
321        }
322    } else if sql_upper.starts_with("UPDATE") {
323        if !sql_upper.contains("SET") {
324            return Err("UPDATE statement missing SET clause".to_string());
325        }
326    } else if sql_upper.starts_with("DELETE") && !sql_upper.contains("FROM") {
327        return Err("DELETE statement missing FROM clause".to_string());
328    }
329
330    // Parameter count check
331    if let Some(expected) = expected_params {
332        let actual = sql.chars().filter(|&c| c == '?').count();
333        if actual != expected {
334            return Err(format!(
335                "Parameter count mismatch: expected {} parameters, found {}",
336                expected, actual
337            ));
338        }
339    }
340
341    Ok(())
342}
343
344fn validate_balanced_parens(sql: &str) -> Result<(), String> {
345    let mut depth: i32 = 0;
346    for (i, ch) in sql.char_indices() {
347        match ch {
348            '(' => depth += 1,
349            ')' => {
350                depth -= 1;
351                if depth < 0 {
352                    return Err(format!(
353                        "Unbalanced parentheses: unexpected ')' at position {}",
354                        i
355                    ));
356                }
357            }
358            _ => {}
359        }
360    }
361    if depth != 0 {
362        return Err(format!("Unbalanced parentheses: {} unclosed '('", depth));
363    }
364    Ok(())
365}
366
367fn validate_string_literals_closed(sql: &str) -> Result<(), String> {
368    let mut in_single = false;
369    let mut in_double = false;
370    let mut prev = '\0';
371
372    for ch in sql.chars() {
373        if prev == '\\' {
374            prev = ch;
375            continue;
376        }
377
378        match ch {
379            '\'' if !in_double => in_single = !in_single,
380            '"' if !in_single => in_double = !in_double,
381            _ => {}
382        }
383        prev = ch;
384    }
385
386    if in_single {
387        return Err("Unclosed single-quoted string literal".to_string());
388    }
389    if in_double {
390        return Err("Unclosed double-quoted string literal".to_string());
391    }
392
393    Ok(())
394}
395
396fn validate_no_injection(sql: &str) -> Result<(), String> {
397    let sql_lower = sql.to_lowercase();
398
399    // 注意:编译期 SQL 内容已由 Rust 字符串字面量解析剥离外层引号,
400    // 因此检测模式不应依赖前导引号字符(如 `"'; DROP TABLE"`)。
401    let injection_patterns: &[&str] = &[
402        // 多语句攻击
403        "drop table",
404        "drop database",
405        "; drop",
406        // 经典注入
407        "or 1=1",
408        "or 1 = 1",
409        "union select",
410        "union all select",
411        // 注释攻击
412        "--",
413        "/*",
414        "*/",
415        // 存储过程注入
416        "xp_cmdshell",
417        "sp_executesql",
418        "exec(",
419        "execute(",
420        // 信息泄露
421        "information_schema",
422        "sys.tables",
423        "sys.columns",
424    ];
425
426    for pattern in injection_patterns {
427        if sql_lower.contains(pattern) {
428            return Err(format!("潜在的 SQL 注入模式被检测到: '{}'", pattern));
429        }
430    }
431
432    Ok(())
433}
434
435// ---------------------------------------------------------------------------
436// `query!` macro — optional real DB verification (gated by `db-verify` feature)
437// ---------------------------------------------------------------------------
438
439/// Compile-time SQL validation with optional real DB verification.
440///
441/// Behavior:
442/// - Always runs the same syntax validation as `sql_string!`.
443/// - When the `db-verify` cargo feature is enabled **AND** the
444///   `SZ_ORM_QUERY_VERIFY=1` environment variable is set at compile time,
445///   connects to the database pointed to by `DATABASE_URL` and runs
446///   `EXPLAIN` (MySQL/PostgreSQL) or `EXPLAIN QUERY PLAN` (SQLite) to verify
447///   the SQL is valid against the actual schema (column names, table names,
448///   joins, etc.).
449/// - Otherwise, falls back to syntax-only validation.
450///
451/// Emits a [`sz_orm_core::queryable::Query`] object wrapping the validated SQL.
452///
453/// # Syntax
454///
455/// ```ignore
456/// use sz_orm_core::queryable::Query;
457/// let q = query!("SELECT id, name FROM users WHERE id = ?");
458/// let rows = q.fetch_all(&mut conn).await?;
459/// ```
460///
461/// # Verification setup
462///
463/// ```bash
464/// export DATABASE_URL="mysql://user:pass@host:3306/db"
465/// export SZ_ORM_QUERY_VERIFY=1
466/// cargo build --features sz-orm-macros/db-verify
467/// ```
468#[proc_macro]
469pub fn query(input: TokenStream) -> TokenStream {
470    let mut tokens = input.into_iter().peekable();
471
472    // P0-1:支持可选类型参数 `query!(T, "SQL")` → `QueryAs::<T>::new(sql)`
473    // 若无类型参数则保持 `query!("SQL")` → `Query::new(sql)`
474    let type_param: Option<TokenStream2> = match tokens.peek() {
475        Some(TokenTree::Ident(_)) | Some(TokenTree::Punct(_)) => {
476            // 收集类型路径(如 `User` 或 `crate::User`)
477            let mut ty_tokens = Vec::new();
478            while let Some(tok) = tokens.peek() {
479                match tok {
480                    TokenTree::Punct(p) if p.as_char() == ',' => break,
481                    TokenTree::Punct(p) if p.as_char() == ':' => {
482                        ty_tokens.push(tokens.next().unwrap());
483                        // 消耗 `:`
484                        if let Some(TokenTree::Punct(p2)) = tokens.peek() {
485                            if p2.as_char() == ':' {
486                                ty_tokens.push(tokens.next().unwrap());
487                            }
488                        }
489                    }
490                    _ => ty_tokens.push(tokens.next().unwrap()),
491                }
492            }
493            // 确认下一个 token 是逗号(类型参数分隔符)
494            match tokens.peek() {
495                Some(TokenTree::Punct(p)) if p.as_char() == ',' => {
496                    tokens.next(); // 消耗逗号
497                    let ts: proc_macro::TokenStream = ty_tokens.into_iter().collect();
498                    Some(TokenStream2::from(ts))
499                }
500                _ => None, // 不是类型参数,回退
501            }
502        }
503        _ => None,
504    };
505
506    // Parse the SQL string literal
507    let sql = match tokens.next() {
508        Some(TokenTree::Literal(lit)) => lit.to_string(),
509        Some(other) => {
510            return compile_error(
511                other.span(),
512                if type_param.is_some() {
513                    "query!(T, \"SQL\"): expected a string literal as the second argument"
514                } else {
515                    "Expected a string literal as the first argument to query!"
516                },
517            );
518        }
519        None => {
520            return compile_error(
521                Span::call_site(),
522                if type_param.is_some() {
523                    "query!(T, \"SQL\"): missing SQL string argument"
524                } else {
525                    "Expected a string literal argument to query!"
526                },
527            );
528        }
529    };
530
531    let sql_content = match strip_string_literal(&sql) {
532        Some(s) => s,
533        None => {
534            return compile_error(
535                Span::call_site(),
536                "query! requires a string literal argument",
537            );
538        }
539    };
540
541    // Syntax validation (shared with sql_string!)
542    if let Err(err_msg) = validate_sql_content(sql_content, None) {
543        return compile_error(Span::call_site(), &err_msg);
544    }
545
546    // Optional real DB verification (only when feature is enabled)
547    #[cfg(feature = "db-verify")]
548    let verify_cols: Option<Vec<(String, String)>> = {
549        match std::env::var("SZ_ORM_QUERY_VERIFY").ok().as_deref() {
550            // 模式 1:连真 DB 执行 EXPLAIN 验证(需 DATABASE_URL),并获取 SELECT 列的实际类型
551            Some("1") => match verify_with_real_db(sql_content) {
552                Ok(cols) => {
553                    // v4.3.0 M1-T3:EXPLAIN 分析 → 编译期性能警告(非阻断)
554                    for warning in analyze_explain(sql_content) {
555                        eprintln!("warning: [sz-orm-explain] {warning}");
556                    }
557                    Some(cols)
558                }
559                Err(err) => {
560                    return compile_error(
561                        Span::call_site(),
562                        &format!("query! real DB verification failed: {}", err),
563                    )
564                }
565            },
566            // 模式 cache:从离线缓存文件查找(无需 DB,适合 CI)
567            Some("cache") => {
568                if let Err(err) = verify_with_cache(sql_content) {
569                    return compile_error(
570                        Span::call_site(),
571                        &format!("query! offline cache verification failed: {}", err),
572                    );
573                }
574                None
575            }
576            _ => None,
577        }
578    };
579    #[cfg(not(feature = "db-verify"))]
580    let _verify_cols: Option<Vec<(String, String)>> = None;
581
582    // Emit the appropriate query object
583    let escaped = sql_content.escape_default().to_string();
584    let base = if let Some(ref ty) = type_param {
585        // query!(T, "SQL") → QueryAs::<T>::new("SQL")
586        format!(
587            "::sz_orm_core::queryable::QueryAs::<{}>::new(\"{}\")",
588            ty, escaped
589        )
590    } else {
591        // query!("SQL") → Query::new("SQL")
592        format!("::sz_orm_core::queryable::Query::new(\"{}\")", escaped)
593    };
594    // db-verify 通过且有类型参数时,附加编译期类型验证块(P0-2)
595    #[cfg(feature = "db-verify")]
596    let output = match (&verify_cols, &type_param) {
597        (Some(cols), Some(ty)) if !cols.is_empty() => {
598            gen_compile_time_type_check(&ty.to_string(), sql_content, cols, &base)
599        }
600        _ => base,
601    };
602    #[cfg(not(feature = "db-verify"))]
603    let output = base;
604    output
605        .parse()
606        .unwrap_or_else(|_| compile_error(Span::call_site(), "Failed to generate query! output"))
607}
608
609/// Strip surrounding quotes from a string literal token's raw representation.
610/// Shared by `sql_string!` and `query!`.
611fn strip_string_literal(raw: &str) -> Option<&str> {
612    if raw.starts_with("r#\"") {
613        Some(&raw[3..raw.len() - 2])
614    } else if raw.starts_with("r\"") {
615        Some(&raw[2..raw.len() - 1])
616    } else if raw.starts_with('"') {
617        Some(&raw[1..raw.len() - 1])
618    } else if raw.starts_with("b\"") || raw.starts_with("b\'") {
619        Some(&raw[2..raw.len() - 1])
620    } else {
621        None
622    }
623}
624
625// ---------------------------------------------------------------------------
626// Real DB verification (only compiled when `db-verify` feature is enabled)
627// ---------------------------------------------------------------------------
628
629#[cfg(feature = "db-verify")]
630fn verify_with_real_db(sql: &str) -> Result<Vec<(String, String)>, String> {
631    let dsn = std::env::var("DATABASE_URL")
632        .map_err(|_| "DATABASE_URL environment variable not set".to_string())?;
633
634    let db_kind =
635        detect_db_kind(&dsn).map_err(|e| format!("Failed to detect DB kind from DSN: {}", e))?;
636
637    // 将 ? 占位符替换为 NULL,使 EXPLAIN 无需绑定参数即可执行。
638    // EXPLAIN 不实际执行查询,NULL 对所有列类型都合法。
639    let sql_no_placeholders = replace_placeholders_with_null(sql);
640
641    // Oracle/SQL Server 使用 EXPLAIN PLAN FOR(不同语法),其余用 EXPLAIN
642    let explain_sql = match db_kind {
643        DbKind::MySql | DbKind::Postgres => format!("EXPLAIN {}", sql_no_placeholders),
644        DbKind::Sqlite => format!("EXPLAIN QUERY PLAN {}", sql_no_placeholders),
645        // Oracle: EXPLAIN PLAN FOR 放入 PLAN_TABLE,再查询结果验证语法
646        DbKind::Oracle => format!("EXPLAIN PLAN FOR {}", sql_no_placeholders),
647        // SQL Server: SET SHOWPLAN_TEXT ON 后执行(不实际运行)
648        DbKind::SqlServer => sql_no_placeholders,
649    };
650
651    // MySQL/PG/SQLite 走 sqlx 异步路径
652    if matches!(db_kind, DbKind::MySql | DbKind::Postgres | DbKind::Sqlite) {
653        let rt = tokio::runtime::Runtime::new()
654            .map_err(|e| format!("Failed to create tokio runtime: {}", e))?;
655        return rt.block_on(async {
656            // 1. EXPLAIN 语法验证
657            if let DbKind::MySql = db_kind {
658                verify_mysql(&dsn, &explain_sql).await?;
659            } else if let DbKind::Postgres = db_kind {
660                verify_postgres(&dsn, &explain_sql).await?;
661            } else {
662                // 由外层 if matches! 保证此处必为 Sqlite
663                verify_sqlite(&dsn, &explain_sql).await?;
664            }
665            // 2. 列名/类型验证(Gap 1 修复)
666            verify_columns(&dsn, db_kind, sql).await?;
667            // 3. 获取 SELECT 列的实际 DB 类型(供编译期类型验证使用)
668            //    SQLite/Oracle/SQL Server 返回空列表(跳过类型级验证)
669            fetch_column_types(&dsn, db_kind, sql).await
670        });
671    }
672
673    // Oracle/SQL Server 走命令行工具验证(避免引入重依赖)
674    if let DbKind::Oracle = db_kind {
675        verify_oracle(&dsn, &explain_sql).map(|_| Vec::new())
676    } else {
677        // 由外层 if matches! 保证此处必为 SqlServer
678        verify_sqlserver(&dsn, &explain_sql).map(|_| Vec::new())
679    }
680}
681
682// ---------------------------------------------------------------------------
683// v4.3.0 M1-T3:EXPLAIN 结果分析(编译期性能警告,非阻断)
684// 在 db-verify 语法/列验证通过后,进一步解析 EXPLAIN 输出,检测
685// 全表扫描与缺失索引,输出编译期警告(eprintln,stable Rust 无 Span::warning)。
686// ---------------------------------------------------------------------------
687
688/// Analyzes EXPLAIN output and returns a list of warning messages.
689///
690/// Only runs analysis on the sqlx path (MySQL/PostgreSQL/SQLite); the
691/// Oracle/SQL Server command-line verification path is skipped. On parse
692/// failure it degrades to an empty list (EXPLAIN syntax has already been
693/// validated by [`verify_with_real_db`], so this does not block compilation).
694#[cfg(feature = "db-verify")]
695fn analyze_explain(sql: &str) -> Vec<String> {
696    let Ok(dsn) = std::env::var("DATABASE_URL") else {
697        return Vec::new();
698    };
699    let Ok(db_kind) = detect_db_kind(&dsn) else {
700        return Vec::new();
701    };
702    if !matches!(db_kind, DbKind::MySql | DbKind::Postgres | DbKind::Sqlite) {
703        return Vec::new();
704    }
705    let sql_no_placeholders = replace_placeholders_with_null(sql);
706    let explain_sql = match db_kind {
707        DbKind::MySql | DbKind::Postgres => format!("EXPLAIN {}", sql_no_placeholders),
708        DbKind::Sqlite => format!("EXPLAIN QUERY PLAN {}", sql_no_placeholders),
709        _ => return Vec::new(),
710    };
711    let rt = match tokio::runtime::Runtime::new() {
712        Ok(rt) => rt,
713        Err(_) => return Vec::new(),
714    };
715    rt.block_on(explain_analysis_raw(&dsn, db_kind, &explain_sql))
716        .unwrap_or_default()
717}
718
719/// Fetches EXPLAIN raw output and parses it into a list of warnings
720#[cfg(feature = "db-verify")]
721async fn explain_analysis_raw(
722    dsn: &str,
723    db_kind: DbKind,
724    explain_sql: &str,
725) -> Result<Vec<String>, String> {
726    let raw = match db_kind {
727        DbKind::MySql => fetch_explain_mysql(dsn, explain_sql).await?,
728        DbKind::Postgres => fetch_explain_postgres(dsn, explain_sql).await?,
729        DbKind::Sqlite => fetch_explain_sqlite(dsn, explain_sql).await?,
730        _ => return Ok(Vec::new()),
731    };
732    let db_type = match db_kind {
733        DbKind::MySql => sz_orm_explain::ExplainDialect::MySql,
734        DbKind::Postgres => sz_orm_explain::ExplainDialect::Postgres,
735        DbKind::Sqlite => sz_orm_explain::ExplainDialect::Sqlite,
736        _ => return Ok(Vec::new()),
737    };
738    let parser = sz_orm_explain::parser_for(db_type)
739        .map_err(|e| format!("no explain parser for db: {e}"))?;
740    let plan = parser
741        .parse(&raw)
742        .map_err(|e| format!("explain parse failed: {e}"))?;
743
744    let mut warnings = Vec::new();
745    if plan.scan_type.is_full_table_scan() {
746        warnings.push(format!(
747            "full table scan detected on table '{}': consider adding an index",
748            plan.table
749        ));
750    } else {
751        // 行数阈值:SZ_ORM_EXPLAIN_ROW_THRESHOLD 环境变量,默认 1000
752        let threshold: u64 = std::env::var("SZ_ORM_EXPLAIN_ROW_THRESHOLD")
753            .ok()
754            .and_then(|v| v.parse().ok())
755            .unwrap_or(1000);
756        if plan.missing_index(threshold) {
757            warnings.push(format!(
758                "missing index on table '{}': estimated rows = {} exceeds threshold {}",
759                plan.table, plan.rows, threshold
760            ));
761        }
762    }
763    Ok(warnings)
764}
765
766/// MySQL: EXPLAIN table rows → `| a | b | ... |` text (compatible with MySqlParser input)
767#[cfg(feature = "db-verify")]
768async fn fetch_explain_mysql(dsn: &str, explain_sql: &str) -> Result<String, String> {
769    let pool = sqlx::MySqlPool::connect(dsn)
770        .await
771        .map_err(|e| format!("MySQL connect failed: {e}"))?;
772    let rows = sqlx::query(sqlx::AssertSqlSafe(explain_sql))
773        .fetch_all(&pool)
774        .await
775        .map_err(|e| format!("MySQL EXPLAIN failed: {e}"))?;
776    let mut out = String::new();
777    for row in &rows {
778        let cells: Vec<String> = (0..row.columns().len())
779            .map(|i| row.try_get::<String, _>(i).unwrap_or_default())
780            .collect();
781        out.push_str(&format!("| {} |\n", cells.join(" | ")));
782    }
783    Ok(out)
784}
785
786/// PostgreSQL: EXPLAIN text lines concatenated
787#[cfg(feature = "db-verify")]
788async fn fetch_explain_postgres(dsn: &str, explain_sql: &str) -> Result<String, String> {
789    let pool = sqlx::PgPool::connect(dsn)
790        .await
791        .map_err(|e| format!("PostgreSQL connect failed: {e}"))?;
792    let rows: Vec<(String,)> = sqlx::query_as::<_, (String,)>(sqlx::AssertSqlSafe(explain_sql))
793        .fetch_all(&pool)
794        .await
795        .map_err(|e| format!("PostgreSQL EXPLAIN failed: {e}"))?;
796    Ok(rows
797        .iter()
798        .map(|(line,)| line.as_str())
799        .collect::<Vec<_>>()
800        .join("\n"))
801}
802
803/// SQLite: EXPLAIN QUERY PLAN four columns (id/parent/notused/detail) → numeric-prefixed text
804#[cfg(feature = "db-verify")]
805async fn fetch_explain_sqlite(dsn: &str, explain_sql: &str) -> Result<String, String> {
806    let pool = sqlx::SqlitePool::connect(dsn)
807        .await
808        .map_err(|e| format!("SQLite connect failed: {e}"))?;
809    let rows = sqlx::query(sqlx::AssertSqlSafe(explain_sql))
810        .fetch_all(&pool)
811        .await
812        .map_err(|e| format!("SQLite EXPLAIN failed: {e}"))?;
813    let mut out = String::new();
814    for row in &rows {
815        let id: i64 = row.try_get(0).unwrap_or_default();
816        let parent: i64 = row.try_get(1).unwrap_or_default();
817        let notused: i64 = row.try_get(2).unwrap_or_default();
818        let detail: String = row.try_get(3).unwrap_or_default();
819        out.push_str(&format!("{id} {parent} {notused} {detail}\n"));
820    }
821    Ok(out)
822}
823
824/// Offline cache verification: looks up verified SQL from the JSON file specified by `SZ_ORM_SQLX_CACHE`.
825///
826/// The cache file format is a JSON array of strings, one verified SQL per entry:
827/// ```json
828/// ["SELECT `id`, `name` FROM `users` WHERE `id` = ?", ...]
829/// ```
830///
831/// To generate it: run `cargo build --features db-verify` (with `SZ_ORM_QUERY_VERIFY=1`) in an
832/// environment that has DB access, or use the `cargo sz-orm prepare` tool to scan `query!` macros
833/// in the project and generate the cache.
834///
835/// In CI, simply set `SZ_ORM_QUERY_VERIFY=cache` + `SZ_ORM_SQLX_CACHE=.sz-orm/query-cache.json`
836/// to perform compile-time SQL verification without connecting to the DB.
837#[cfg(feature = "db-verify")]
838fn verify_with_cache(sql: &str) -> Result<(), String> {
839    let cache_path = std::env::var("SZ_ORM_SQLX_CACHE").map_err(|_| {
840        "SZ_ORM_SQLX_CACHE not set. \
841             Set it to the path of a JSON file containing verified SQL statements, \
842             e.g. SZ_ORM_SQLX_CACHE=.sz-orm/query-cache.json"
843            .to_string()
844    })?;
845
846    let cache_content = std::fs::read_to_string(&cache_path).map_err(|e| {
847        format!(
848            "Failed to read cache file '{}': {}. \
849             Run `cargo sz-orm prepare` or build with SZ_ORM_QUERY_VERIFY=1 to generate it.",
850            cache_path, e
851        )
852    })?;
853
854    // 支持两种格式:JSON 数组 或 每行一条 SQL 的文本文件
855    let verified: Vec<String> = serde_json::from_str(&cache_content).unwrap_or_else(|_| {
856        cache_content
857            .lines()
858            .map(|l| l.trim().to_string())
859            .filter(|l| !l.is_empty() && !l.starts_with('#'))
860            .collect()
861    });
862
863    if verified.iter().any(|v| v.trim() == sql.trim()) {
864        Ok(())
865    } else {
866        Err(format!(
867            "SQL not found in offline cache ({} entries): \"{}\". \
868             Add it to the cache by running with SZ_ORM_QUERY_VERIFY=1 first.",
869            verified.len(),
870            truncate_sql(sql, 80)
871        ))
872    }
873}
874
875/// Truncates SQL for error message display
876#[cfg(feature = "db-verify")]
877fn truncate_sql(sql: &str, max: usize) -> String {
878    if sql.len() <= max {
879        sql.to_string()
880    } else {
881        format!("{}...", &sql[..max])
882    }
883}
884
885#[cfg(feature = "db-verify")]
886#[derive(Debug, Clone, Copy, PartialEq, Eq)]
887enum DbKind {
888    MySql,
889    Postgres,
890    Sqlite,
891    Oracle,
892    SqlServer,
893}
894
895/// Replaces `?` placeholders in SQL with `NULL`, skipping `?` inside string literals.
896///
897/// EXPLAIN does not actually execute the query; using NULL in place of parameters
898/// validates syntax and table/column existence while avoiding the sqlx prepared-statement
899/// requirement of binding parameters.
900#[cfg(feature = "db-verify")]
901fn replace_placeholders_with_null(sql: &str) -> String {
902    let mut result = String::with_capacity(sql.len() + 16);
903    let mut in_single_quote = false;
904    let mut in_double_quote = false;
905    let mut prev = '\0';
906
907    for ch in sql.chars() {
908        if prev == '\\' {
909            // 转义字符:直接追加
910            result.push(ch);
911            prev = ch;
912            continue;
913        }
914        match ch {
915            '\'' if !in_double_quote => in_single_quote = !in_single_quote,
916            '"' if !in_single_quote => in_double_quote = !in_double_quote,
917            '?' if !in_single_quote && !in_double_quote => {
918                result.push_str("NULL");
919                prev = ch;
920                continue;
921            }
922            _ => {}
923        }
924        result.push(ch);
925        prev = ch;
926    }
927    result
928}
929
930#[cfg(feature = "db-verify")]
931fn detect_db_kind(dsn: &str) -> Result<DbKind, String> {
932    let lower = dsn.to_lowercase();
933    if lower.starts_with("mysql://") {
934        Ok(DbKind::MySql)
935    } else if lower.starts_with("postgres://") || lower.starts_with("postgresql://") {
936        Ok(DbKind::Postgres)
937    } else if lower.starts_with("sqlite://") || lower.starts_with("sqlite:") {
938        Ok(DbKind::Sqlite)
939    } else if lower.starts_with("oracle://") || lower.starts_with("oracle:") {
940        Ok(DbKind::Oracle)
941    } else if lower.starts_with("sqlserver://")
942        || lower.starts_with("mssql://")
943        || lower.starts_with("tds://")
944    {
945        Ok(DbKind::SqlServer)
946    } else {
947        Err(format!("Unsupported DSN scheme: {}", dsn))
948    }
949}
950
951#[cfg(feature = "db-verify")]
952async fn verify_mysql(dsn: &str, explain_sql: &str) -> Result<(), String> {
953    let pool = sqlx::MySqlPool::connect(dsn)
954        .await
955        .map_err(|e| format!("MySQL connect failed: {}", e))?;
956    sqlx::query(sqlx::AssertSqlSafe(explain_sql))
957        .execute(&pool)
958        .await
959        .map_err(|e| format!("MySQL EXPLAIN failed: {}", e))?;
960    Ok(())
961}
962
963#[cfg(feature = "db-verify")]
964async fn verify_postgres(dsn: &str, explain_sql: &str) -> Result<(), String> {
965    let pool = sqlx::PgPool::connect(dsn)
966        .await
967        .map_err(|e| format!("PostgreSQL connect failed: {}", e))?;
968    sqlx::query(sqlx::AssertSqlSafe(explain_sql))
969        .execute(&pool)
970        .await
971        .map_err(|e| format!("PostgreSQL EXPLAIN failed: {}", e))?;
972    Ok(())
973}
974
975#[cfg(feature = "db-verify")]
976async fn verify_sqlite(dsn: &str, explain_sql: &str) -> Result<(), String> {
977    let pool = sqlx::SqlitePool::connect(dsn)
978        .await
979        .map_err(|e| format!("SQLite connect failed: {}", e))?;
980    sqlx::query(sqlx::AssertSqlSafe(explain_sql))
981        .execute(&pool)
982        .await
983        .map_err(|e| format!("SQLite EXPLAIN failed: {}", e))?;
984    Ok(())
985}
986
987// ========================================================================
988// Gap 1 修复:列名/类型验证(在 EXPLAIN 语法验证通过后执行)
989// ========================================================================
990
991/// Extracts table names and column references from SQL and queries information_schema to verify column existence.
992///
993/// EXPLAIN has already validated syntax and table existence; this function further verifies:
994/// - Whether columns referenced in SELECT/WHERE/ORDER BY/GROUP BY exist in the corresponding tables
995/// - Does not verify columns qualified by table aliases (handled by EXPLAIN)
996/// - Only verifies explicit column names other than `*`
997///
998/// # Supported DBs
999///
1000/// MySQL / PostgreSQL / SQLite (Oracle/SQL Server skip this step)
1001#[cfg(feature = "db-verify")]
1002async fn verify_columns(dsn: &str, db_kind: DbKind, sql: &str) -> Result<(), String> {
1003    // SQLite 的 information_schema 支持有限,跳过
1004    if matches!(db_kind, DbKind::Sqlite | DbKind::Oracle | DbKind::SqlServer) {
1005        return Ok(());
1006    }
1007
1008    let tables = extract_tables(sql);
1009    let columns = extract_columns(sql);
1010
1011    if tables.is_empty() || columns.is_empty() {
1012        return Ok(());
1013    }
1014
1015    match db_kind {
1016        DbKind::MySql => verify_columns_mysql(dsn, &tables, &columns, sql).await,
1017        DbKind::Postgres => verify_columns_postgres(dsn, &tables, &columns, sql).await,
1018        _ => Ok(()),
1019    }
1020}
1021
1022/// Extracts table names from the SQL FROM clause (supports aliases and JOIN)
1023#[cfg(feature = "db-verify")]
1024fn extract_tables(sql: &str) -> Vec<String> {
1025    let mut tables = Vec::new();
1026    let upper = sql.to_uppercase();
1027
1028    // 查找 FROM ... WHERE/ORDER/GROUP/LIMIT/HAVING/JOIN 之间的内容
1029    let from_idx = match upper.find("FROM") {
1030        Some(i) => i,
1031        None => return tables,
1032    };
1033
1034    let end_patterns = ["WHERE", "ORDER", "GROUP", "LIMIT", "HAVING", "UNION"];
1035    let end_idx = end_patterns
1036        .iter()
1037        .filter_map(|p| {
1038            // 按单词边界查找,避免 "ORDER" 误匹配 "user_id" 等标识符中的子串
1039            let mut search_start = 0;
1040            while let Some(i) = upper[search_start..].find(*p) {
1041                let abs_i = search_start + i;
1042                let before = upper[..abs_i].chars().last().unwrap_or(' ');
1043                let after = upper[abs_i + p.len()..].chars().next().unwrap_or(' ');
1044                if !before.is_alphanumeric()
1045                    && !after.is_alphanumeric()
1046                    && before != '_'
1047                    && after != '_'
1048                {
1049                    return Some(abs_i);
1050                }
1051                search_start = abs_i + p.len();
1052            }
1053            None
1054        })
1055        .filter(|&i| i > from_idx)
1056        .min()
1057        .unwrap_or(sql.len());
1058
1059    let from_clause = &sql[from_idx + 4..end_idx];
1060
1061    // 按逗号、换行、JOIN 关键字分割(忽略大小写)
1062    let join_split = {
1063        let lower = from_clause.to_lowercase();
1064        let mut result = String::with_capacity(from_clause.len());
1065        let mut i = 0;
1066        let bytes = from_clause.as_bytes();
1067        let lower_bytes = lower.as_bytes();
1068        while i < bytes.len() {
1069            let mut matched = false;
1070            for join_kw in &[
1071                " join ",
1072                " inner join ",
1073                " left join ",
1074                " right join ",
1075                " left outer join ",
1076                " right outer join ",
1077                " cross join ",
1078                " full join ",
1079                " full outer join ",
1080            ] {
1081                let kw = join_kw.as_bytes();
1082                if i + kw.len() <= bytes.len() && &lower_bytes[i..i + kw.len()] == kw {
1083                    result.push(',');
1084                    i += kw.len();
1085                    matched = true;
1086                    break;
1087                }
1088            }
1089            if !matched {
1090                result.push(bytes[i] as char);
1091                i += 1;
1092            }
1093        }
1094        result
1095    };
1096    let parts: Vec<&str> = join_split.split([',', '\n']).collect();
1097
1098    for part in parts {
1099        let part = part.trim();
1100        if part.is_empty() {
1101            continue;
1102        }
1103        // 取第一个词作为表名(忽略别名)
1104        let table_word = part
1105            .split_whitespace()
1106            .next()
1107            .unwrap_or(part)
1108            .trim_end_matches([',', ';']);
1109        // 去除反引号/双引号
1110        let clean = table_word.trim_matches(|c| c == '`' || c == '"');
1111        if !clean.is_empty()
1112            && !matches!(
1113                clean.to_uppercase().as_str(),
1114                "INNER"
1115                    | "LEFT"
1116                    | "RIGHT"
1117                    | "OUTER"
1118                    | "CROSS"
1119                    | "FULL"
1120                    | "NATURAL"
1121                    | "ON"
1122                    | "USING"
1123                    | "AS"
1124            )
1125        {
1126            tables.push(clean.to_lowercase());
1127        }
1128    }
1129
1130    tables
1131}
1132
1133/// Extracts unqualified column name references from SQL (in SELECT/WHERE/ORDER BY/GROUP BY)
1134#[cfg(feature = "db-verify")]
1135fn extract_columns(sql: &str) -> Vec<String> {
1136    let mut columns = Vec::new();
1137    let upper = sql.to_uppercase();
1138
1139    // 收集各子句中的标识符
1140    let mut collect_from_segment = |segment: &str| {
1141        // 简单的标识符提取:匹配 `\w+` 模式的词
1142        // 排除 SQL 关键字和已限定的列(table.col)
1143        let keywords = [
1144            "SELECT",
1145            "FROM",
1146            "WHERE",
1147            "AND",
1148            "OR",
1149            "NOT",
1150            "IN",
1151            "IS",
1152            "NULL",
1153            "LIKE",
1154            "BETWEEN",
1155            "AS",
1156            "ON",
1157            "JOIN",
1158            "INNER",
1159            "LEFT",
1160            "RIGHT",
1161            "OUTER",
1162            "CROSS",
1163            "FULL",
1164            "NATURAL",
1165            "ORDER",
1166            "BY",
1167            "GROUP",
1168            "HAVING",
1169            "LIMIT",
1170            "OFFSET",
1171            "ASC",
1172            "DESC",
1173            "DISTINCT",
1174            "COUNT",
1175            "SUM",
1176            "AVG",
1177            "MIN",
1178            "MAX",
1179            "CASE",
1180            "WHEN",
1181            "THEN",
1182            "ELSE",
1183            "END",
1184            "COALESCE",
1185            "NULLIF",
1186            "CAST",
1187            "TRUE",
1188            "FALSE",
1189            "INSERT",
1190            "INTO",
1191            "VALUES",
1192            "UPDATE",
1193            "SET",
1194            "DELETE",
1195            "CREATE",
1196            "TABLE",
1197            "INDEX",
1198            "IF",
1199            "EXISTS",
1200            "PRIMARY",
1201            "KEY",
1202            "REFERENCES",
1203            "FOREIGN",
1204        ];
1205
1206        for word in segment.split(|c: char| !c.is_alphanumeric() && c != '_') {
1207            if word.is_empty() || word.len() < 2 {
1208                continue;
1209            }
1210            let w = word.to_uppercase();
1211            if keywords.contains(&w.as_str()) {
1212                continue;
1213            }
1214            // 跳过纯数字
1215            if word.chars().all(|c| c.is_ascii_digit()) {
1216                continue;
1217            }
1218            // 跳过已限定的列(table.col)— 这些由 EXPLAIN 验证
1219            // 检查前面是否有 `.`
1220            let pos = segment.find(word).unwrap_or(0);
1221            if pos > 0 && segment.chars().nth(pos - 1) == Some('.') {
1222                continue;
1223            }
1224            // 跳过 *
1225            if word == "*" {
1226                continue;
1227            }
1228            let lower = word.to_lowercase();
1229            if !columns.contains(&lower) {
1230                columns.push(lower);
1231            }
1232        }
1233    };
1234
1235    // 收集 SELECT 列(FROM 之前)
1236    if let Some(from_idx) = upper.find("FROM") {
1237        if let Some(sel_idx) = upper.find("SELECT") {
1238            let sel_segment = &sql[sel_idx + 6..from_idx];
1239            collect_from_segment(sel_segment);
1240        }
1241    }
1242
1243    // 收集 WHERE 列
1244    if let Some(where_idx) = upper.find("WHERE") {
1245        let end_idx = ["ORDER", "GROUP", "LIMIT", "HAVING", "UNION"]
1246            .iter()
1247            .filter_map(|p| upper.find(p))
1248            .filter(|&i| i > where_idx)
1249            .min()
1250            .unwrap_or(sql.len());
1251        collect_from_segment(&sql[where_idx + 5..end_idx]);
1252    }
1253
1254    // 收集 ORDER BY 列
1255    if let Some(order_idx) = upper.find("ORDER BY") {
1256        let end_idx = ["GROUP", "LIMIT", "HAVING", "UNION"]
1257            .iter()
1258            .filter_map(|p| upper.find(p))
1259            .filter(|&i| i > order_idx)
1260            .min()
1261            .unwrap_or(sql.len());
1262        collect_from_segment(&sql[order_idx + 8..end_idx]);
1263    }
1264
1265    columns
1266}
1267
1268#[cfg(feature = "db-verify")]
1269async fn verify_columns_mysql(
1270    dsn: &str,
1271    tables: &[String],
1272    columns: &[String],
1273    sql: &str,
1274) -> Result<(), String> {
1275    let pool = sqlx::MySqlPool::connect(dsn)
1276        .await
1277        .map_err(|e| format!("MySQL connect failed: {}", e))?;
1278
1279    for col in columns {
1280        // 查询 information_schema.COLUMNS
1281        let rows = sqlx::query(
1282            "SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS \
1283             WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = ?",
1284        )
1285        .bind(col)
1286        .fetch_all(&pool)
1287        .await
1288        .map_err(|e| format!("MySQL column lookup failed for '{}': {}", col, e))?;
1289
1290        if rows.is_empty() {
1291            // 尝试检查是否是数据库函数(如 NOW, COUNT 等)
1292            if is_sql_function(col) {
1293                continue;
1294            }
1295            return Err(format!(
1296                "query! column verification failed: column '{}' not found in any table of the current database. \
1297                 SQL: {}",
1298                col,
1299                truncate_sql(sql, 120)
1300            ));
1301        }
1302
1303        // 验证列至少存在于一个 FROM 表中(如果有表信息)
1304        if !tables.is_empty() {
1305            let found_in_table = rows.iter().any(|row| {
1306                let table_name: String = row.get("TABLE_NAME");
1307                tables.iter().any(|t| t == &table_name.to_lowercase())
1308            });
1309            if !found_in_table {
1310                let available: Vec<String> = rows.iter().map(|r| r.get("TABLE_NAME")).collect();
1311                return Err(format!(
1312                    "query! column verification failed: column '{}' exists but not in FROM table(s) {:?}. \
1313                     Found in: {:?}. SQL: {}",
1314                    col,
1315                    tables,
1316                    available,
1317                    truncate_sql(sql, 120)
1318                ));
1319            }
1320        }
1321    }
1322
1323    Ok(())
1324}
1325
1326#[cfg(feature = "db-verify")]
1327async fn verify_columns_postgres(
1328    dsn: &str,
1329    tables: &[String],
1330    columns: &[String],
1331    sql: &str,
1332) -> Result<(), String> {
1333    let pool = sqlx::PgPool::connect(dsn)
1334        .await
1335        .map_err(|e| format!("PostgreSQL connect failed: {}", e))?;
1336
1337    for col in columns {
1338        let rows = sqlx::query(
1339            "SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS \
1340             WHERE TABLE_CATALOG = CURRENT_CATALOG AND COLUMN_NAME = $1",
1341        )
1342        .bind(col)
1343        .fetch_all(&pool)
1344        .await
1345        .map_err(|e| format!("PostgreSQL column lookup failed for '{}': {}", col, e))?;
1346
1347        if rows.is_empty() && !is_sql_function(col) {
1348            return Err(format!(
1349                "query! column verification failed: column '{}' not found in any table of the current database. \
1350                 SQL: {}",
1351                col,
1352                truncate_sql(sql, 120)
1353            ));
1354        }
1355
1356        if !tables.is_empty() && !rows.is_empty() {
1357            let found_in_table = rows.iter().any(|row| {
1358                // PG information_schema 返回小写列名,按索引取列避免大小写问题
1359                let table_name: String = row.try_get(0).unwrap_or_default();
1360                tables.iter().any(|t| t == &table_name.to_lowercase())
1361            });
1362            if !found_in_table {
1363                let available: Vec<String> = rows
1364                    .iter()
1365                    .map(|r| r.try_get::<String, _>(0).unwrap_or_default())
1366                    .collect();
1367                return Err(format!(
1368                    "query! column verification failed: column '{}' exists but not in FROM table(s) {:?}. \
1369                     Found in: {:?}. SQL: {}",
1370                    col, tables, available,
1371                    truncate_sql(sql, 120)
1372                ));
1373            }
1374        }
1375    }
1376
1377    Ok(())
1378}
1379
1380// ---------------------------------------------------------------------------
1381// 列类型获取(P0-2)
1382//
1383// 获取 SELECT 列的实际 DB 类型(列名 → 类型名),由 `query_as!`/`query!(T, ...)`
1384// 宏嵌入到生成的编译期验证代码中:用户代码在 const 上下文中将实际类型与
1385// 结构体 `__sz_orm_column_types()` 期望值对比,不匹配即编译失败。
1386// ---------------------------------------------------------------------------
1387
1388/// Fetches the actual DB type list of SELECT columns `(column name, DATA_TYPE/udt_name)`.
1389///
1390/// Only MySQL/PostgreSQL are supported (SQLite/Oracle/SQL Server return an empty list and skip type-level verification).
1391#[cfg(feature = "db-verify")]
1392async fn fetch_column_types(
1393    dsn: &str,
1394    db_kind: DbKind,
1395    sql: &str,
1396) -> Result<Vec<(String, String)>, String> {
1397    if !matches!(db_kind, DbKind::MySql | DbKind::Postgres) {
1398        return Ok(Vec::new());
1399    }
1400
1401    let tables = extract_tables(sql);
1402    let columns = extract_columns(sql);
1403    if tables.is_empty() || columns.is_empty() {
1404        return Ok(Vec::new());
1405    }
1406
1407    match db_kind {
1408        DbKind::MySql => fetch_column_types_mysql(dsn, &tables, &columns).await,
1409        DbKind::Postgres => fetch_column_types_postgres(dsn, &tables, &columns).await,
1410        _ => Ok(Vec::new()),
1411    }
1412}
1413
1414#[cfg(feature = "db-verify")]
1415async fn fetch_column_types_mysql(
1416    dsn: &str,
1417    tables: &[String],
1418    columns: &[String],
1419) -> Result<Vec<(String, String)>, String> {
1420    let pool = sqlx::MySqlPool::connect(dsn)
1421        .await
1422        .map_err(|e| format!("MySQL connect failed for type fetch: {}", e))?;
1423
1424    let mut result = Vec::new();
1425    for col in columns {
1426        let rows = sqlx::query(
1427            "SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE \
1428             FROM INFORMATION_SCHEMA.COLUMNS \
1429             WHERE TABLE_SCHEMA = DATABASE() AND COLUMN_NAME = ?",
1430        )
1431        .bind(col)
1432        .fetch_all(&pool)
1433        .await
1434        .map_err(|e| format!("MySQL type lookup failed for '{}': {}", col, e))?;
1435
1436        // 取 FROM 表中的类型(多表同名列时取第一个匹配)
1437        let ty = rows
1438            .iter()
1439            .find(|row| {
1440                let tn: String = row.get("TABLE_NAME");
1441                tables.iter().any(|t| t == &tn.to_lowercase())
1442            })
1443            .and_then(|r| r.try_get::<String, _>("DATA_TYPE").ok());
1444        if let Some(ty) = ty {
1445            result.push((col.to_lowercase(), ty));
1446        }
1447    }
1448    Ok(result)
1449}
1450
1451#[cfg(feature = "db-verify")]
1452async fn fetch_column_types_postgres(
1453    dsn: &str,
1454    tables: &[String],
1455    columns: &[String],
1456) -> Result<Vec<(String, String)>, String> {
1457    let pool = sqlx::PgPool::connect(dsn)
1458        .await
1459        .map_err(|e| format!("PostgreSQL connect failed for type fetch: {}", e))?;
1460
1461    let mut result = Vec::new();
1462    for col in columns {
1463        let rows = sqlx::query(
1464            "SELECT TABLE_NAME, COLUMN_NAME, udt_name \
1465             FROM INFORMATION_SCHEMA.COLUMNS \
1466             WHERE TABLE_CATALOG = CURRENT_CATALOG AND COLUMN_NAME = $1",
1467        )
1468        .bind(col)
1469        .fetch_all(&pool)
1470        .await
1471        .map_err(|e| format!("PostgreSQL type lookup failed for '{}': {}", col, e))?;
1472
1473        let ty = rows
1474            .iter()
1475            .find(|row| {
1476                // PG information_schema 返回小写列名,按索引取列
1477                let tn: String = row.try_get(0).unwrap_or_default();
1478                tables.iter().any(|t| t == &tn.to_lowercase())
1479            })
1480            .and_then(|r| r.try_get::<String, _>(2).ok());
1481        if let Some(ty) = ty {
1482            result.push((col.to_lowercase(), ty));
1483        }
1484    }
1485    Ok(result)
1486}
1487
1488/// Generates a compile-time type validation code block: `{ const _: () = { ...checks... }; <query expression> }`.
1489///
1490/// The validation logic runs in a const context (`panic!` triggers a compilation failure, achieving true compile-time interception):
1491/// 1. The column count must match the struct field count;
1492/// 2. Each SELECT column name must exist among the struct fields (compared against `__sz_orm_column_types()`);
1493/// 3. Each column's actual DB type must be compatible with the struct field type (`__sz_orm_const_types_compatible`).
1494///
1495/// `record_type` is the first argument of `query_as!` (e.g. `User` / `crate::User`);
1496/// the generated code references the const fn produced by the derive macro via
1497/// `<record type>::__sz_orm_column_types()` (so the record type must `#[derive(FromQueryResult)]`).
1498#[cfg(feature = "db-verify")]
1499fn gen_compile_time_type_check(
1500    record_type: &str,
1501    sql: &str,
1502    cols: &[(String, String)],
1503    query_expr: &str,
1504) -> String {
1505    let n = cols.len();
1506    let sql_esc = sql.escape_default().to_string();
1507    let mut checks = String::new();
1508    checks.push_str(&format!(
1509        "if exp.len() != {} {{ panic!(\"sz-orm compile-time type check failed for `{}`: SELECT returns {} columns but struct field count differs\"); }}",
1510        n, sql_esc, n
1511    ));
1512    for (i, (name, ty)) in cols.iter().enumerate() {
1513        let name_esc = name.escape_default().to_string();
1514        let ty_esc = ty.escape_default().to_string();
1515        checks.push_str(&format!(
1516            "if !::sz_orm_core::__sz_orm_const_str_eq(exp[{}].0, \"{}\") {{ panic!(\"sz-orm compile-time type check failed for `{}`: SELECT column #{} `{}` not found in struct fields\"); }}",
1517            i, name_esc, sql_esc, i, name_esc
1518        ));
1519        checks.push_str(&format!(
1520            "if !::sz_orm_core::__sz_orm_const_types_compatible(\"{}\", exp[{}].1) {{ panic!(\"sz-orm compile-time type check failed for `{}`: column `{}` type mismatch (db type `{}` not compatible with struct field type)\"); }}",
1521            ty_esc, i, sql_esc, name_esc, ty_esc
1522        ));
1523    }
1524    format!(
1525        "{{ const _: () = {{ let exp = <{}>::__sz_orm_column_types(); {} }}; {} }}",
1526        record_type, checks, query_expr
1527    )
1528}
1529
1530/// Common SQL function name list (should not be verified as column names)
1531#[cfg(feature = "db-verify")]
1532fn is_sql_function(name: &str) -> bool {
1533    matches!(
1534        name.to_uppercase().as_str(),
1535        "NOW"
1536            | "CURRENT_TIMESTAMP"
1537            | "CURRENT_DATE"
1538            | "CURRENT_TIME"
1539            | "COUNT"
1540            | "SUM"
1541            | "AVG"
1542            | "MIN"
1543            | "MAX"
1544            | "COALESCE"
1545            | "NULLIF"
1546            | "CAST"
1547            | "CONVERT"
1548            | "IFNULL"
1549            | "NVL"
1550            | "UPPER"
1551            | "LOWER"
1552            | "LENGTH"
1553            | "TRIM"
1554            | "SUBSTRING"
1555            | "CONCAT"
1556            | "REPLACE"
1557            | "ROUND"
1558            | "CEIL"
1559            | "FLOOR"
1560            | "ABS"
1561            | "MOD"
1562            | "POWER"
1563            | "SQRT"
1564            | "LOG"
1565            | "EXP"
1566            | "DATE"
1567            | "YEAR"
1568            | "MONTH"
1569            | "DAY"
1570            | "HOUR"
1571            | "MINUTE"
1572            | "SECOND"
1573            | "NOW()"
1574            | "UUID"
1575            | "RANDOM"
1576            | "MD5"
1577            | "TRUE"
1578            | "FALSE"
1579            | "NULL"
1580    )
1581}
1582
1583/// Oracle compile-time verification: runs EXPLAIN PLAN FOR via the sqlplus command-line tool
1584///
1585/// DSN format: `oracle://user:pass@host:port/service` (optional `?sysdba=1`)
1586/// e.g.: `oracle://sys:test123@127.0.0.1:1521/freepdb1.FALSE?sysdba=1`
1587#[cfg(feature = "db-verify")]
1588fn verify_oracle(dsn: &str, explain_sql: &str) -> Result<(), String> {
1589    let parsed = parse_oracle_dsn(dsn)?;
1590    let mut conn_str = format!(
1591        "{}/{}@{}:{}/{}",
1592        parsed.user, parsed.password, parsed.host, parsed.port, parsed.service
1593    );
1594    if parsed.sysdba {
1595        conn_str.push_str(" AS SYSDBA");
1596    }
1597    let full_script = format!(
1598        "SET HEADING OFF FEEDBACK OFF ECHO OFF;\n\
1599         EXPLAIN PLAN FOR {};\n\
1600         SELECT COUNT(*) FROM plan_table WHERE statement_id = (SELECT MAX(statement_id) FROM plan_table);\n\
1601         EXIT;\n",
1602        explain_sql
1603    );
1604    let connect_script = format!("connect {}\n{}", conn_str, full_script);
1605    let output = std::process::Command::new("sqlplus")
1606        .args(["-S", "-L", "/nolog"])
1607        .stdin(std::process::Stdio::piped())
1608        .stdout(std::process::Stdio::piped())
1609        .stderr(std::process::Stdio::piped())
1610        .spawn()
1611        .map_err(|e| format!("sqlplus not found (Oracle client required): {}", e))?;
1612    use std::io::Write;
1613    let mut child = output;
1614    if let Some(mut stdin) = child.stdin.take() {
1615        stdin
1616            .write_all(connect_script.as_bytes())
1617            .map_err(|e| format!("sqlplus stdin write failed: {}", e))?;
1618    }
1619    let out = child
1620        .wait_with_output()
1621        .map_err(|e| format!("sqlplus wait failed: {}", e))?;
1622    let stdout = String::from_utf8_lossy(&out.stdout);
1623    let stderr = String::from_utf8_lossy(&out.stderr);
1624    if !out.status.success() || stdout.contains("ORA-") || stdout.contains("SP2-") {
1625        return Err(format!(
1626            "Oracle EXPLAIN failed: stdout={} stderr={}",
1627            stdout.trim(),
1628            stderr.trim()
1629        ));
1630    }
1631    Ok(())
1632}
1633
1634/// SQL Server compile-time verification: runs SET SHOWPLAN_TEXT ON via the sqlcmd command-line tool
1635///
1636/// DSN format: `sqlserver://user:pass@host:port/db`
1637/// e.g.: `sqlserver://test:JkbC2jsaWAYDe2Gz@sh-mssql-adrul9nm.sql.tencentcdb.com:22527/test`
1638#[cfg(feature = "db-verify")]
1639fn verify_sqlserver(dsn: &str, explain_sql: &str) -> Result<(), String> {
1640    let parsed = parse_sqlserver_dsn(dsn)?;
1641    let query = format!("SET SHOWPLAN_TEXT ON;\n{}", explain_sql);
1642    let out = std::process::Command::new("sqlcmd")
1643        .args([
1644            "-S",
1645            &format!("{},{}", parsed.host, parsed.port),
1646            "-U",
1647            &parsed.user,
1648            "-d",
1649            &parsed.database,
1650            "-Q",
1651            &query,
1652            "-h",
1653            "-1",
1654            "-W",
1655        ])
1656        .env("SQLCMDPASSWORD", &parsed.password)
1657        .output()
1658        .map_err(|e| format!("sqlcmd not found (SQL Server client required): {}", e))?;
1659    let stdout = String::from_utf8_lossy(&out.stdout);
1660    let stderr = String::from_utf8_lossy(&out.stderr);
1661    if !out.status.success() || stdout.contains("Msg ") || stdout.contains("Level ") {
1662        return Err(format!(
1663            "SQL Server SHOWPLAN failed: stdout={} stderr={}",
1664            stdout.trim(),
1665            stderr.trim()
1666        ));
1667    }
1668    Ok(())
1669}
1670
1671/// Oracle DSN parse result
1672#[cfg(feature = "db-verify")]
1673struct OracleDsn {
1674    user: String,
1675    password: String,
1676    host: String,
1677    port: u16,
1678    service: String,
1679    sysdba: bool,
1680}
1681
1682/// Parses oracle://user:pass@host:port/service?sysdba=1
1683#[cfg(feature = "db-verify")]
1684fn parse_oracle_dsn(dsn: &str) -> Result<OracleDsn, String> {
1685    let raw = dsn
1686        .strip_prefix("oracle://")
1687        .or_else(|| dsn.strip_prefix("oracle:"))
1688        .ok_or_else(|| format!("Invalid Oracle DSN: {}", dsn))?;
1689    // 分离 query
1690    let (auth_host_service, query) = match raw.find('?') {
1691        Some(idx) => (&raw[..idx], &raw[idx + 1..]),
1692        None => (raw, ""),
1693    };
1694    let sysdba = query
1695        .split('&')
1696        .any(|p| p == "sysdba=1" || p == "sysdba=true");
1697    // user:pass@host:port/service
1698    let at = auth_host_service
1699        .find('@')
1700        .ok_or_else(|| format!("Oracle DSN missing '@': {}", dsn))?;
1701    let (user_pass, host_port_service) = (&auth_host_service[..at], &auth_host_service[at + 1..]);
1702    let colon = user_pass
1703        .find(':')
1704        .ok_or_else(|| format!("Oracle DSN missing password separator: {}", dsn))?;
1705    let (user, password) = (&user_pass[..colon], &user_pass[colon + 1..]);
1706    let (host_port, service) = match host_port_service.rfind('/') {
1707        Some(idx) => (&host_port_service[..idx], &host_port_service[idx + 1..]),
1708        None => return Err(format!("Oracle DSN missing service name: {}", dsn)),
1709    };
1710    let (host, port) = match host_port.find(':') {
1711        Some(idx) => (
1712            &host_port[..idx],
1713            host_port[idx + 1..]
1714                .parse::<u16>()
1715                .map_err(|_| format!("Oracle DSN invalid port: {}", dsn))?,
1716        ),
1717        None => (host_port, 1521u16),
1718    };
1719    Ok(OracleDsn {
1720        user: user.to_string(),
1721        password: password.to_string(),
1722        host: host.to_string(),
1723        port,
1724        service: service.to_string(),
1725        sysdba,
1726    })
1727}
1728
1729/// SQL Server DSN parse result
1730#[cfg(feature = "db-verify")]
1731struct SqlServerDsn {
1732    user: String,
1733    password: String,
1734    host: String,
1735    port: u16,
1736    database: String,
1737}
1738
1739/// Parses sqlserver://user:pass@host:port/db
1740#[cfg(feature = "db-verify")]
1741fn parse_sqlserver_dsn(dsn: &str) -> Result<SqlServerDsn, String> {
1742    let raw = dsn
1743        .strip_prefix("sqlserver://")
1744        .or_else(|| dsn.strip_prefix("mssql://"))
1745        .or_else(|| dsn.strip_prefix("tds://"))
1746        .ok_or_else(|| format!("Invalid SQL Server DSN: {}", dsn))?;
1747    let at = raw
1748        .find('@')
1749        .ok_or_else(|| format!("SQL Server DSN missing '@': {}", dsn))?;
1750    let (user_pass, host_port_db) = (&raw[..at], &raw[at + 1..]);
1751    let colon = user_pass
1752        .find(':')
1753        .ok_or_else(|| format!("SQL Server DSN missing password separator: {}", dsn))?;
1754    let (user, password) = (&user_pass[..colon], &user_pass[colon + 1..]);
1755    let (host_port, database) = match host_port_db.rfind('/') {
1756        Some(idx) => (&host_port_db[..idx], &host_port_db[idx + 1..]),
1757        None => return Err(format!("SQL Server DSN missing database: {}", dsn)),
1758    };
1759    let (host, port) = match host_port.find(':') {
1760        Some(idx) => (
1761            &host_port[..idx],
1762            host_port[idx + 1..]
1763                .parse::<u16>()
1764                .map_err(|_| format!("SQL Server DSN invalid port: {}", dsn))?,
1765        ),
1766        None => (host_port, 1433u16),
1767    };
1768    Ok(SqlServerDsn {
1769        user: user.to_string(),
1770        password: password.to_string(),
1771        host: host.to_string(),
1772        port,
1773        database: database.to_string(),
1774    })
1775}
1776
1777// ---------------------------------------------------------------------------
1778// Helpers
1779// ---------------------------------------------------------------------------
1780
1781/// Create a compile_error! token stream
1782fn compile_error(span: Span, msg: &str) -> TokenStream {
1783    // emit: compile_error!("msg")
1784    let mut ts = TokenStream::new();
1785    ts.extend([
1786        TokenTree::Ident(Ident::new("compile_error", span)),
1787        TokenTree::Punct(Punct::new('!', Spacing::Alone)),
1788        TokenTree::Group(Group::new(
1789            Delimiter::Parenthesis,
1790            TokenStream::from(TokenTree::Literal(Literal::string(msg))),
1791        )),
1792    ]);
1793    ts
1794}
1795
1796// ---------------------------------------------------------------------------
1797// typed_query! — Diesel 风格强类型 AST 宏
1798// ---------------------------------------------------------------------------
1799
1800/// Diesel-style strongly-typed AST macro (coexists with `sql_string!` / `query!`).
1801///
1802/// # Design
1803///
1804/// Accepts a `table { col1: Type, col2: Type, ... }` declaration and generates:
1805/// 1. A `table` module
1806/// 2. A zero-size marker type for each column (e.g. `table::id`)
1807/// 3. An impl of the `TypedColumn` trait that lifts the column name + Rust type into the type system
1808///
1809/// This way, `typed_query!(SELECT id FROM users WHERE name = ?)` can, at compile time:
1810/// - Verify that `id` / `name` columns exist in the `users` table declaration
1811/// - Verify that the Rust type of the `?` parameter matches the column's declared type
1812///
1813/// # Usage
1814///
1815/// ```ignore
1816/// use sz_orm_macros::typed_query;
1817///
1818/// // 1. Declare table schema (compile-time generates column marker types)
1819/// typed_query! {
1820///     table users {
1821///         id: i64,
1822///         name: String,
1823///         email: String,
1824///         age: i32,
1825///     }
1826/// }
1827///
1828/// // 2. Compile-time SELECT validation: column names must exist in the users table
1829/// let sql = typed_query!(SELECT id, name FROM users WHERE age > ?);
1830/// // ❌ compile error: unknown column 'foo' in table 'users'
1831/// // let sql = typed_query!(SELECT foo FROM users);
1832/// ```
1833#[proc_macro]
1834pub fn typed_query(input: TokenStream) -> TokenStream {
1835    let tokens: Vec<TokenTree> = input.into_iter().collect();
1836
1837    // 分支 1:table 声明
1838    if tokens.iter().any(|t| {
1839        if let TokenTree::Ident(id) = t {
1840            id.to_string() == "table"
1841        } else {
1842            false
1843        }
1844    }) {
1845        return parse_table_decl(&tokens);
1846    }
1847
1848    // 分支 2:SELECT 表达式
1849    if tokens.iter().any(|t| {
1850        if let TokenTree::Ident(id) = t {
1851            id.to_string().eq_ignore_ascii_case("SELECT")
1852        } else {
1853            false
1854        }
1855    }) {
1856        return parse_typed_select(&tokens);
1857    }
1858
1859    compile_error(
1860        Span::call_site(),
1861        "typed_query! expects either `table name { ... }` declaration or `SELECT ... FROM ...` expression",
1862    )
1863}
1864
1865/// Parses a `table name { col: Type, ... }` declaration
1866fn parse_table_decl(tokens: &[TokenTree]) -> TokenStream {
1867    // 期望格式:table <ident> { <ident> : <ident> [, ...] }
1868    let mut idx = 0;
1869
1870    // 跳过 'table' 关键字
1871    if idx >= tokens.len() {
1872        return compile_error(Span::call_site(), "expected table name after 'table'");
1873    }
1874    if let TokenTree::Ident(id) = &tokens[idx] {
1875        if id.to_string() != "table" {
1876            return compile_error(id.span(), "expected 'table' keyword");
1877        }
1878    }
1879    idx += 1;
1880
1881    // 表名
1882    let table_name = if idx < tokens.len() {
1883        if let TokenTree::Ident(id) = &tokens[idx] {
1884            id.to_string()
1885        } else {
1886            return compile_error(tokens[idx].span(), "expected table name identifier");
1887        }
1888    } else {
1889        return compile_error(Span::call_site(), "expected table name");
1890    };
1891    idx += 1;
1892
1893    // 表体({} 内)
1894    let body_group = if idx < tokens.len() {
1895        if let TokenTree::Group(g) = &tokens[idx] {
1896            if g.delimiter() != Delimiter::Brace {
1897                return compile_error(g.span(), "expected '{' after table name");
1898            }
1899            g.clone()
1900        } else {
1901            return compile_error(tokens[idx].span(), "expected '{' after table name");
1902        }
1903    } else {
1904        return compile_error(Span::call_site(), "expected table body in '{ }'");
1905    };
1906
1907    // 解析列声明
1908    let body_tokens: Vec<TokenTree> = body_group.stream().into_iter().collect();
1909    let columns = match parse_column_list(&body_tokens) {
1910        Ok(c) => c,
1911        Err(e) => return compile_error(Span::call_site(), &e),
1912    };
1913
1914    // 使用 quote! 构建类型安全的 TokenStream
1915    let table_ident = proc_macro2::Ident::new(&table_name, Span::call_site().into());
1916    let table_name_lit = table_name.as_str();
1917
1918    // 为每列构建标记类型 + trait 实现
1919    let col_impls: Vec<TokenStream2> = columns
1920        .iter()
1921        .map(|(col_name, col_type)| {
1922            let col_ident =
1923                proc_macro2::Ident::new(&format!("col_{}", col_name), Span::call_site().into());
1924            let col_name_lit = col_name.as_str();
1925            // 解析类型字符串为 TokenStream(quote! 会处理)
1926            let rust_type: TokenStream2 = col_type.parse().unwrap_or_else(|_| quote! { () });
1927            quote! {
1928                #[derive(Debug, Clone, Copy)]
1929                pub struct #col_ident;
1930                impl ::sz_orm_core::typed::TypedColumn for #col_ident {
1931                    const NAME: &'static str = #col_name_lit;
1932                    type Table = table;
1933                    type RustType = #rust_type;
1934                    type SqlType = <#rust_type as ::sz_orm_core::typed_ast::InferSqlType>::SqlType;
1935                }
1936            }
1937        })
1938        .collect();
1939
1940    // schema 常量条目
1941    let schema_entries: Vec<TokenStream2> = columns
1942        .iter()
1943        .map(|(n, t)| {
1944            let n_lit = n.as_str();
1945            let t_lit = t.as_str();
1946            quote! { (#n_lit, #t_lit) }
1947        })
1948        .collect();
1949
1950    let schema_const_ident = proc_macro2::Ident::new(
1951        &format!("__SZ_ORM_TYPED_SCHEMA_{}", table_name.to_uppercase()),
1952        Span::call_site().into(),
1953    );
1954
1955    let expanded = quote! {
1956        pub mod #table_ident {
1957            use super::*;
1958            pub struct table;
1959            impl ::sz_orm_core::typed::TypedTable for table {
1960                const NAME: &'static str = #table_name_lit;
1961            }
1962            #(#col_impls)*
1963        }
1964        const #schema_const_ident: &[(&str, &str)] = &[#(#schema_entries),*];
1965    };
1966
1967    expanded.into()
1968}
1969
1970/// Parses a column declaration list: `col: Type, col2: Type2, ...`
1971fn parse_column_list(tokens: &[TokenTree]) -> Result<Vec<(String, String)>, String> {
1972    let mut cols = Vec::new();
1973    let mut i = 0;
1974    while i < tokens.len() {
1975        // 列名
1976        let col_name = if let TokenTree::Ident(id) = &tokens[i] {
1977            id.to_string()
1978        } else {
1979            return Err(format!("expected column name at position {}", i));
1980        };
1981        i += 1;
1982
1983        // 冒号
1984        if i >= tokens.len() {
1985            return Err(format!("expected ':' after column '{}'", col_name));
1986        }
1987        if let TokenTree::Punct(p) = &tokens[i] {
1988            if p.as_char() != ':' {
1989                return Err(format!("expected ':' after column '{}'", col_name));
1990            }
1991        } else {
1992            return Err(format!("expected ':' after column '{}'", col_name));
1993        }
1994        i += 1;
1995
1996        // 类型(可能是 ident 或 path,如 String / i64 / Option<i64>)
1997        // 简化处理:收集直到遇到 ',' 或末尾
1998        let mut type_str = String::new();
1999        let mut depth = 0;
2000        while i < tokens.len() {
2001            match &tokens[i] {
2002                TokenTree::Punct(p) => {
2003                    if p.as_char() == ',' && depth == 0 {
2004                        i += 1;
2005                        break;
2006                    } else if p.as_char() == '<' || p.as_char() == '(' {
2007                        depth += 1;
2008                        type_str.push(p.as_char());
2009                    } else if p.as_char() == '>' || p.as_char() == ')' {
2010                        depth -= 1;
2011                        type_str.push(p.as_char());
2012                    } else {
2013                        type_str.push(p.as_char());
2014                    }
2015                }
2016                TokenTree::Ident(id) => {
2017                    if !type_str.is_empty() && !type_str.ends_with('<') && !type_str.ends_with('(')
2018                    {
2019                        type_str.push(' ');
2020                    }
2021                    type_str.push_str(&id.to_string());
2022                }
2023                _ => {}
2024            }
2025            i += 1;
2026        }
2027
2028        cols.push((col_name, type_str.trim().to_string()));
2029    }
2030    Ok(cols)
2031}
2032
2033/// Parses a `SELECT col1, col2 FROM table WHERE col = ?` expression
2034///
2035/// Validates column names against the table schema (via compile-time constant lookup).
2036fn parse_typed_select(tokens: &[TokenTree]) -> TokenStream {
2037    // 收集所有 ident 与 literal,构造 SQL 字符串
2038    let mut sql_parts: Vec<String> = Vec::new();
2039    let mut table_name: Option<String> = None;
2040    let mut in_from = false;
2041
2042    for (i, t) in tokens.iter().enumerate() {
2043        match t {
2044            TokenTree::Ident(id) => {
2045                let s = id.to_string();
2046                if s.eq_ignore_ascii_case("SELECT") {
2047                    sql_parts.push("SELECT".to_string());
2048                } else if s.eq_ignore_ascii_case("FROM") {
2049                    in_from = true;
2050                    sql_parts.push("FROM".to_string());
2051                } else if s.eq_ignore_ascii_case("WHERE")
2052                    || s.eq_ignore_ascii_case("AND")
2053                    || s.eq_ignore_ascii_case("OR")
2054                    || s.eq_ignore_ascii_case("LIMIT")
2055                    || s.eq_ignore_ascii_case("OFFSET")
2056                    || s.eq_ignore_ascii_case("ORDER")
2057                    || s.eq_ignore_ascii_case("BY")
2058                    || s.eq_ignore_ascii_case("GROUP")
2059                    || s.eq_ignore_ascii_case("HAVING")
2060                    || s.eq_ignore_ascii_case("JOIN")
2061                    || s.eq_ignore_ascii_case("INNER")
2062                    || s.eq_ignore_ascii_case("LEFT")
2063                    || s.eq_ignore_ascii_case("RIGHT")
2064                    || s.eq_ignore_ascii_case("ON")
2065                    || s.eq_ignore_ascii_case("AS")
2066                    || s.eq_ignore_ascii_case("ASC")
2067                    || s.eq_ignore_ascii_case("DESC")
2068                    || s.eq_ignore_ascii_case("DISTINCT")
2069                    || s.eq_ignore_ascii_case("NOT")
2070                    || s.eq_ignore_ascii_case("NULL")
2071                    || s.eq_ignore_ascii_case("IN")
2072                    || s.eq_ignore_ascii_case("BETWEEN")
2073                    || s.eq_ignore_ascii_case("LIKE")
2074                    || s.eq_ignore_ascii_case("IS")
2075                {
2076                    sql_parts.push(s.to_uppercase());
2077                } else if in_from && table_name.is_none() {
2078                    // FROM 后第一个 ident 是表名
2079                    table_name = Some(s.clone());
2080                    sql_parts.push(s.clone());
2081                } else {
2082                    sql_parts.push(s.clone());
2083                }
2084            }
2085            TokenTree::Literal(lit) => {
2086                sql_parts.push(lit.to_string());
2087            }
2088            TokenTree::Punct(p) => {
2089                let c = p.as_char();
2090                // SQL 中常见标点:, ; * ? = > < ( ) . 等
2091                let part = if c == ',' {
2092                    ",".to_string()
2093                } else if c == '?' {
2094                    "?".to_string()
2095                } else if c == '*' {
2096                    "*".to_string()
2097                } else if c == '=' {
2098                    "=".to_string()
2099                } else if c == '>' {
2100                    ">".to_string()
2101                } else if c == '<' {
2102                    "<".to_string()
2103                } else if c == '.' {
2104                    ".".to_string()
2105                } else if c == ';' {
2106                    ";".to_string()
2107                } else {
2108                    c.to_string()
2109                };
2110                sql_parts.push(part);
2111            }
2112            TokenTree::Group(g) => {
2113                // 处理 group(如 (1, 2, 3))
2114                let inner: String = g.stream().to_string();
2115                let delim = match g.delimiter() {
2116                    Delimiter::Parenthesis => "(",
2117                    Delimiter::Brace => "{",
2118                    Delimiter::Bracket => "[",
2119                    Delimiter::None => "",
2120                };
2121                let close = match g.delimiter() {
2122                    Delimiter::Parenthesis => ")",
2123                    Delimiter::Brace => "}",
2124                    Delimiter::Bracket => "]",
2125                    Delimiter::None => "",
2126                };
2127                sql_parts.push(format!("{}{}{}", delim, inner, close));
2128            }
2129        }
2130        // 单空格分隔(去重多个空格由 trim 处理)
2131        let _ = i;
2132    }
2133
2134    let sql = sql_parts
2135        .join(" ")
2136        .replace(", ", ",")
2137        .replace(" ,", ",")
2138        .replace("= ", "=")
2139        .replace(" =", "=")
2140        .replace("> ", ">")
2141        .replace(" >", ">")
2142        .replace("< ", "<")
2143        .replace(" <", "<")
2144        .replace("  ", " ");
2145
2146    // 验证 SQL 语法
2147    if let Err(e) = validate_sql_content(&sql, None) {
2148        return compile_error(
2149            Span::call_site(),
2150            &format!("typed_query! SQL validation failed: {}", e),
2151        );
2152    }
2153
2154    // 生成 SQL 字符串字面量
2155    let mut ts = TokenStream::new();
2156    let lit = Literal::string(&sql);
2157    ts.extend([TokenTree::Literal(lit)]);
2158    ts
2159}
2160
2161// ---------------------------------------------------------------------------
2162// schema! — Compile-time SQL schema generator
2163// ---------------------------------------------------------------------------
2164
2165/// Compile-time SQL schema generator.
2166///
2167/// Parses a SQL `CREATE TABLE` statement and generates typed table declarations
2168/// equivalent to `typed_query! { table ... }`.
2169///
2170/// # Syntax
2171///
2172/// ```ignore
2173/// use sz_orm_macros::schema;
2174///
2175/// schema! {
2176///     "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT)"
2177/// }
2178/// ```
2179///
2180/// Generates code equivalent to the following manual declaration:
2181/// ```ignore
2182/// typed_query! {
2183///     table users {
2184///         id: i64,
2185///         name: String,
2186///         email: Option<String>,
2187///     }
2188/// }
2189/// ```
2190#[proc_macro]
2191/// Typed raw SQL query macro (SQLx `query_as!` style).
2192///
2193/// Usage: `query_as!(RecordType, "SELECT col1, col2 FROM table WHERE id = ?")`
2194///
2195/// Generates `sz_orm_core::queryable::QueryAs::<RecordType>::new("SELECT ...")`.
2196/// When the `db-verify` feature + `SZ_ORM_QUERY_VERIFY=1` is set, connects to the real DB
2197/// and runs EXPLAIN to verify SQL validity.
2198///
2199/// **Runtime column-name validation** (P0-2): `QueryAs::fetch_all` compares the column names
2200/// returned by the DB against `RecordType::row_desc()` (auto-generated by `#[derive(FromQueryResult)]`).
2201/// If a SELECT column is not among the struct fields, returns `DbError::QueryError`.
2202///
2203/// # Example
2204///
2205/// ```ignore
2206/// #[derive(FromQueryResult)]
2207/// struct User { id: i64, name: String }
2208///
2209/// let q = query_as!(User, "SELECT id, name FROM users WHERE id = 1");
2210/// let users: Vec<User> = q.fetch_all(&mut conn).await?;
2211/// ```
2212pub fn query_as(input: TokenStream) -> TokenStream {
2213    let mut tokens = input.into_iter().peekable();
2214
2215    // 解析记录类型(第一个标识符/路径,如 User 或 crate::User)
2216    let mut record_type = String::new();
2217    loop {
2218        match tokens.next() {
2219            Some(TokenTree::Ident(ident)) => {
2220                record_type.push_str(&ident.to_string());
2221            }
2222            Some(TokenTree::Punct(p)) if p.as_char() == ':' => {
2223                // 处理 :: 路径分隔符
2224                record_type.push_str("::");
2225                // 跳过第二个 :
2226                if let Some(TokenTree::Punct(p2)) = tokens.peek() {
2227                    if p2.as_char() == ':' {
2228                        let _ = tokens.next();
2229                    }
2230                }
2231            }
2232            Some(TokenTree::Punct(p)) if p.as_char() == ',' => break,
2233            Some(TokenTree::Punct(p)) if p.as_char() == ',' => break,
2234            Some(other) => {
2235                return compile_error(
2236                    other.span(),
2237                    "query_as! 第一个参数必须是记录类型,如 query_as!(User, \"SELECT ...\")",
2238                );
2239            }
2240            None => {
2241                return compile_error(
2242                    Span::call_site(),
2243                    "query_as! 需要两个参数:query_as!(RecordType, \"SELECT ...\")",
2244                );
2245            }
2246        }
2247    }
2248
2249    // 解析 SQL 字符串字面量
2250    let sql_raw = match tokens.next() {
2251        Some(TokenTree::Literal(lit)) => lit.to_string(),
2252        Some(other) => {
2253            return compile_error(other.span(), "query_as! 第二个参数必须是 SQL 字符串字面量");
2254        }
2255        None => {
2256            return compile_error(
2257                Span::call_site(),
2258                "query_as! 需要两个参数:query_as!(RecordType, \"SELECT ...\")",
2259            );
2260        }
2261    };
2262
2263    let sql_content = match strip_string_literal(&sql_raw) {
2264        Some(s) => s,
2265        None => {
2266            return compile_error(Span::call_site(), "query_as! 的 SQL 参数必须是字符串字面量");
2267        }
2268    };
2269
2270    // 语法验证
2271    if let Err(err_msg) = validate_sql_content(sql_content, None) {
2272        return compile_error(Span::call_site(), &err_msg);
2273    }
2274
2275    // db-verify 验证
2276    #[cfg(feature = "db-verify")]
2277    let verify_cols: Option<Vec<(String, String)>> = {
2278        match std::env::var("SZ_ORM_QUERY_VERIFY").ok().as_deref() {
2279            // 模式 1:连真 DB 执行 EXPLAIN 验证,并获取 SELECT 列的实际类型
2280            Some("1") => match verify_with_real_db(sql_content) {
2281                Ok(cols) => Some(cols),
2282                Err(err) => {
2283                    return compile_error(
2284                        Span::call_site(),
2285                        &format!("query_as! real DB verification failed: {}", err),
2286                    )
2287                }
2288            },
2289            // 模式 cache:从离线缓存文件查找(无需 DB,适合 CI)
2290            Some("cache") => {
2291                if let Err(err) = verify_with_cache(sql_content) {
2292                    return compile_error(
2293                        Span::call_site(),
2294                        &format!("query_as! offline cache verification failed: {}", err),
2295                    );
2296                }
2297                None
2298            }
2299            _ => None,
2300        }
2301    };
2302    #[cfg(not(feature = "db-verify"))]
2303    let _verify_cols: Option<Vec<(String, String)>> = None;
2304
2305    // 生成 QueryAs::<T>::new("...")
2306    // db-verify 通过时,附加编译期类型验证块(P0-2):
2307    // const 上下文中将 DB 实际列类型与结构体 __sz_orm_column_types() 对比,
2308    // 不匹配则 const panic → 编译失败。
2309    let escaped = sql_content.escape_default();
2310    let base = format!(
2311        "::sz_orm_core::queryable::QueryAs::<{}>::new(\"{}\")",
2312        record_type, escaped
2313    );
2314    #[cfg(feature = "db-verify")]
2315    let output = match &verify_cols {
2316        Some(cols) if !cols.is_empty() => {
2317            gen_compile_time_type_check(&record_type, sql_content, cols, &base)
2318        }
2319        _ => base,
2320    };
2321    #[cfg(not(feature = "db-verify"))]
2322    let output = base;
2323    output
2324        .parse()
2325        .unwrap_or_else(|_| compile_error(Span::call_site(), "Failed to generate query_as output"))
2326}
2327
2328#[proc_macro]
2329pub fn schema(input: TokenStream) -> TokenStream {
2330    let mut tokens = input.into_iter().peekable();
2331
2332    // 解析 SQL 字符串字面量
2333    let sql_raw = match tokens.next() {
2334        Some(TokenTree::Literal(lit)) => lit.to_string(),
2335        Some(other) => {
2336            return compile_error(
2337                other.span(),
2338                "Expected a string literal as the argument to schema!",
2339            );
2340        }
2341        None => {
2342            return compile_error(
2343                Span::call_site(),
2344                "Expected a string literal argument to schema!",
2345            );
2346        }
2347    };
2348
2349    let sql = match strip_string_literal(&sql_raw) {
2350        Some(s) => s,
2351        None => {
2352            return compile_error(
2353                Span::call_site(),
2354                "schema! requires a string literal argument",
2355            );
2356        }
2357    };
2358
2359    // 解析 CREATE TABLE
2360    let (table_name, columns) = match parse_create_table(sql) {
2361        Ok(v) => v,
2362        Err(e) => return compile_error(Span::call_site(), &e),
2363    };
2364
2365    // 生成代码(与 parse_table_decl 一致)
2366    let table_ident = proc_macro2::Ident::new(&table_name, Span::call_site().into());
2367    let table_name_lit = table_name.as_str();
2368
2369    let col_impls: Vec<TokenStream2> = columns
2370        .iter()
2371        .map(|(col_name, col_type)| {
2372            let col_ident =
2373                proc_macro2::Ident::new(&format!("col_{}", col_name), Span::call_site().into());
2374            let col_name_lit = col_name.as_str();
2375            let rust_type: TokenStream2 = col_type.parse().unwrap_or_else(|_| quote! { () });
2376            quote! {
2377                #[derive(Debug, Clone, Copy)]
2378                pub struct #col_ident;
2379                impl ::sz_orm_core::typed::TypedColumn for #col_ident {
2380                    const NAME: &'static str = #col_name_lit;
2381                    type Table = table;
2382                    type RustType = #rust_type;
2383                    type SqlType = <#rust_type as ::sz_orm_core::typed_ast::InferSqlType>::SqlType;
2384                }
2385            }
2386        })
2387        .collect();
2388
2389    let schema_entries: Vec<TokenStream2> = columns
2390        .iter()
2391        .map(|(n, t)| {
2392            let n_lit = n.as_str();
2393            let t_lit = t.as_str();
2394            quote! { (#n_lit, #t_lit) }
2395        })
2396        .collect();
2397
2398    let schema_const_ident = proc_macro2::Ident::new(
2399        &format!("__SZ_ORM_TYPED_SCHEMA_{}", table_name.to_uppercase()),
2400        Span::call_site().into(),
2401    );
2402
2403    let expanded = quote! {
2404        pub mod #table_ident {
2405            use super::*;
2406            pub struct table;
2407            impl ::sz_orm_core::typed::TypedTable for table {
2408                const NAME: &'static str = #table_name_lit;
2409            }
2410            #(#col_impls)*
2411        }
2412        const #schema_const_ident: &[(&str, &str)] = &[#(#schema_entries),*];
2413    };
2414
2415    expanded.into()
2416}
2417
2418/// Parses a SQL `CREATE TABLE` statement and returns (table name, Vec<(column name, Rust type string)>).
2419///
2420/// Supported syntax:
2421/// - `CREATE TABLE [IF NOT EXISTS] <name> ( ... )`
2422/// - Table/column names may be backtick-quoted, double-quoted, or unquoted
2423/// - Skips PRIMARY KEY / FOREIGN KEY / CONSTRAINT / UNIQUE / INDEX / KEY constraint lines
2424/// - Column definitions are split by top-level commas (nested parentheses like DECIMAL(10,2) are not split)
2425fn parse_create_table(sql: &str) -> Result<(String, Vec<(String, String)>), String> {
2426    let trimmed = sql.trim();
2427    let upper = trimmed.to_uppercase();
2428
2429    // 必须以 CREATE TABLE 开头
2430    if !upper.starts_with("CREATE TABLE") {
2431        return Err("schema! expects a CREATE TABLE statement".to_string());
2432    }
2433
2434    // 跳过 "CREATE TABLE"
2435    let mut rest = &trimmed["CREATE TABLE".len()..];
2436
2437    // 跳过可选的 "IF NOT EXISTS"
2438    let rest_upper = rest.trim_start().to_uppercase();
2439    if rest_upper.starts_with("IF NOT EXISTS") {
2440        rest = &rest.trim_start()["IF NOT EXISTS".len()..];
2441    }
2442
2443    rest = rest.trim_start();
2444
2445    // 解析表名(可能带反引号、双引号或无引号)
2446    let (table_name, after_name) = parse_identifier(rest)?;
2447    let rest = after_name.trim_start();
2448
2449    // 找到列定义起始的 '(' 与匹配的最后一个 ')'
2450    let paren_start = rest
2451        .find('(')
2452        .ok_or_else(|| "CREATE TABLE missing '(' for column definitions".to_string())?;
2453    let paren_end = rest
2454        .rfind(')')
2455        .ok_or_else(|| "CREATE TABLE missing ')' for column definitions".to_string())?;
2456    if paren_end <= paren_start {
2457        return Err("CREATE TABLE has malformed parentheses".to_string());
2458    }
2459
2460    let cols_str = &rest[paren_start + 1..paren_end];
2461
2462    // 按顶层逗号分隔列定义(注意嵌套括号,如 DECIMAL(10,2))
2463    let col_defs = split_top_level_commas(cols_str);
2464
2465    let mut columns = Vec::new();
2466    for def in col_defs {
2467        let def = def.trim();
2468        if def.is_empty() {
2469            continue;
2470        }
2471
2472        // 跳过约束定义行
2473        let def_upper = def.to_uppercase();
2474        if def_upper.starts_with("PRIMARY KEY")
2475            || def_upper.starts_with("FOREIGN KEY")
2476            || def_upper.starts_with("CONSTRAINT")
2477            || def_upper.starts_with("UNIQUE")
2478            || def_upper.starts_with("INDEX")
2479            || def_upper.starts_with("KEY")
2480        {
2481            continue;
2482        }
2483
2484        // 解析列名
2485        let (col_name, after_col) = parse_identifier(def)?;
2486        let rest = after_col.trim_start();
2487
2488        // 解析类型(取第一个 token,去掉括号参数)
2489        let (sql_type, after_type) = parse_type_token(rest)?;
2490        let rest = after_type.trim();
2491
2492        // 判断 nullability:NOT NULL 或 PRIMARY KEY 隐含 NOT NULL
2493        let rest_upper = rest.to_uppercase();
2494        let not_null = rest_upper.contains("NOT NULL") || rest_upper.contains("PRIMARY KEY");
2495        let rust_type = sql_type_to_rust(&sql_type, !not_null);
2496
2497        columns.push((col_name, rust_type));
2498    }
2499
2500    Ok((table_name, columns))
2501}
2502
2503/// Parses an identifier: supports backtick-quoted, double-quoted, or unquoted forms.
2504/// Returns (identifier, remaining string).
2505fn parse_identifier(s: &str) -> Result<(String, &str), String> {
2506    let s = s.trim_start();
2507    if s.is_empty() {
2508        return Err("expected identifier".to_string());
2509    }
2510
2511    let bytes = s.as_bytes();
2512    match bytes[0] {
2513        b'`' => {
2514            let end = s[1..]
2515                .find('`')
2516                .ok_or_else(|| "unterminated backtick-quoted identifier".to_string())?;
2517            let ident = s[1..1 + end].to_string();
2518            Ok((ident, &s[1 + end + 1..]))
2519        }
2520        b'"' => {
2521            let end = s[1..]
2522                .find('"')
2523                .ok_or_else(|| "unterminated double-quoted identifier".to_string())?;
2524            let ident = s[1..1 + end].to_string();
2525            Ok((ident, &s[1 + end + 1..]))
2526        }
2527        _ => {
2528            let end = s
2529                .find(|c: char| !c.is_alphanumeric() && c != '_')
2530                .unwrap_or(s.len());
2531            if end == 0 {
2532                return Err(format!("invalid identifier: '{}'", s));
2533            }
2534            let ident = s[..end].to_string();
2535            Ok((ident, &s[end..]))
2536        }
2537    }
2538}
2539
2540/// Parses a type token: takes the first identifier, optionally followed by parenthesized parameters (e.g. VARCHAR(255) → VARCHAR).
2541/// Returns (type name, remaining string).
2542fn parse_type_token(s: &str) -> Result<(String, &str), String> {
2543    let s = s.trim_start();
2544    if s.is_empty() {
2545        return Err("expected column type".to_string());
2546    }
2547
2548    let end = s.find(|c: char| !c.is_alphabetic()).unwrap_or(s.len());
2549    if end == 0 {
2550        return Err(format!("invalid type: '{}'", s));
2551    }
2552    let type_name = s[..end].to_string();
2553    let mut rest = &s[end..];
2554
2555    // 跳过可选的括号参数,如 (255) 或 (10,2)
2556    rest = rest.trim_start();
2557    if rest.starts_with('(') {
2558        let close = rest
2559            .find(')')
2560            .ok_or_else(|| "unterminated type parameter list".to_string())?;
2561        rest = &rest[close + 1..];
2562    }
2563
2564    Ok((type_name, rest))
2565}
2566
2567/// Splits a string by top-level commas (does not enter nested parentheses).
2568fn split_top_level_commas(s: &str) -> Vec<String> {
2569    let mut parts = Vec::new();
2570    let mut depth: i32 = 0;
2571    let mut current = String::new();
2572
2573    for ch in s.chars() {
2574        match ch {
2575            '(' => {
2576                depth += 1;
2577                current.push(ch);
2578            }
2579            ')' => {
2580                depth -= 1;
2581                current.push(ch);
2582            }
2583            ',' if depth == 0 => {
2584                parts.push(std::mem::take(&mut current));
2585            }
2586            _ => {
2587                current.push(ch);
2588            }
2589        }
2590    }
2591
2592    if !current.trim().is_empty() {
2593        parts.push(current);
2594    }
2595
2596    parts
2597}
2598
2599/// Maps a SQL type to a Rust type string.
2600///
2601/// Matching rule: takes the first token of the type name (dropping parenthesized parameters),
2602/// matched case-insensitively. Unrecognized types default to `String`. If `nullable == true`,
2603/// wraps in `Option<T>`.
2604fn sql_type_to_rust(sql_type: &str, nullable: bool) -> String {
2605    let upper = sql_type.to_uppercase();
2606    let rust = match upper.as_str() {
2607        // 8 字节整数
2608        "BIGINT" | "INT8" => "i64",
2609        // 4 字节整数(INT/INTEGER/INT4/SERIAL)
2610        "INT" | "INTEGER" | "INT4" | "SERIAL" => "i32",
2611        // 2 字节整数
2612        "SMALLINT" | "INT2" | "SMALLSERIAL" => "i16",
2613        // 1 字节整数
2614        "TINYINT" => "i8",
2615        // 浮点(4 字节)
2616        "FLOAT" | "REAL" | "FLOAT4" => "f32",
2617        // 浮点(8 字节)/ 定点数
2618        "DOUBLE" | "DOUBLE PRECISION" | "FLOAT8" | "DECIMAL" | "NUMERIC" => "f64",
2619        // 布尔
2620        "BOOLEAN" | "BOOL" => "bool",
2621        // 二进制(与 schema_gen::sql_type_to_rust 保持一致)
2622        "BLOB" | "BYTEA" | "BINARY" | "VARBINARY" => "Vec<u8>",
2623        // 字符串/日期/JSON/UUID(统一映射到 String,运行时再解析)
2624        "VARCHAR" | "TEXT" | "CHAR" | "CHARACTER" | "CLOB" | "UUID" | "DATE" | "TIME"
2625        | "DATETIME" | "TIMESTAMP" | "JSON" | "JSONB" => "String",
2626        _ => "String",
2627    };
2628
2629    if nullable {
2630        format!("Option<{}>", rust)
2631    } else {
2632        rust.to_string()
2633    }
2634}
2635
2636// ---------------------------------------------------------------------------
2637// `#[derive(Schema)]` — auto-generate table structure from a struct
2638// ---------------------------------------------------------------------------
2639
2640/// Derive macro: auto-generates table structure info from a Rust struct.
2641///
2642/// Parses `#[table(name = "...")]` and `#[column(...)]` attributes
2643/// and generates a `Schema` trait impl, enabling runtime reflection of table name and column info.
2644///
2645/// # Supported attributes
2646///
2647/// - `#[table(name = "users")]` — specifies the table name (defaults to the snake_case form of the struct name)
2648/// - `#[column(name = "user_id")]` — specifies the column name (defaults to the field name)
2649/// - `#[column(type = "VARCHAR(255)")]` — specifies the SQL type
2650/// - `#[column(primary_key)]` — marks the primary key
2651/// - `#[column(nullable)]` — explicitly marks the field as allowing NULL
2652/// - `#[column(skip)]` — skips this field, no schema entry is generated
2653/// - `#[column(default = "0")]` — marks the field as having a default value
2654///
2655/// # Type inference
2656///
2657/// The field's Rust type is automatically mapped to a SQL type:
2658/// - `i64`/`u64` → `BIGINT`
2659/// - `i32`/`u32` → `INTEGER`
2660/// - `String` → `TEXT`
2661/// - `f64` → `DOUBLE`
2662/// - `bool` → `BOOLEAN`
2663/// - `Vec<u8>` → `BLOB`
2664/// - `Option<T>` → same as `T`, but marked as nullable
2665#[proc_macro_derive(Schema, attributes(table, column))]
2666pub fn derive_schema(input: TokenStream) -> TokenStream {
2667    let input = parse_macro_input!(input as syn::DeriveInput);
2668    derive::derive_schema_impl(input).into()
2669}
2670
2671// ---------------------------------------------------------------------------
2672// `#[derive(GraphQLModel)]` — auto-generate `impl GraphQLModelInfo`
2673// ---------------------------------------------------------------------------
2674
2675/// Derive macro: auto-generates an `sz_orm_graphql::schema_gen::GraphQLModelInfo` impl.
2676///
2677/// Extracts field metadata (field name + Rust type + nullability) from a `#[derive(GraphQLModel)]` struct
2678/// for use by `SchemaGenerator::from_model`. Zero runtime overhead.
2679///
2680/// # Supported attributes
2681///
2682/// - `#[table(name = "users")]` — specifies the table name (defaults to the snake_case form of the struct name)
2683/// - `#[column(skip)]` — skips this field
2684/// - `#[column(name = "custom_name")]` — specifies the column name
2685///
2686/// # Example
2687///
2688/// ```ignore
2689/// use sz_orm_macros::GraphQLModel;
2690///
2691/// #[derive(GraphQLModel)]
2692/// #[table(name = "users")]
2693/// struct User {
2694///     id: i64,
2695///     name: String,
2696///     email: Option<String>,
2697/// }
2698/// ```
2699#[proc_macro_derive(GraphQLModel, attributes(table, column))]
2700pub fn derive_graphql_model(input: TokenStream) -> TokenStream {
2701    let input = parse_macro_input!(input as syn::DeriveInput);
2702    derive::derive_graphql_model_impl(input).into()
2703}
2704
2705// ---------------------------------------------------------------------------
2706// `#[derive(Builder)]` — auto-generate builder pattern code
2707// ---------------------------------------------------------------------------
2708
2709/// Derive macro: auto-generates builder pattern code.
2710///
2711/// Generates an `XxxBuilder` type for the target struct, including:
2712/// - `new()` constructs an empty builder
2713/// - A setter method for each field
2714/// - A `build()` method that returns `Result<T, String>`
2715///
2716/// # Supported attributes
2717///
2718/// - `#[builder(skip)]` — skips this field (no setter is generated, uses Default)
2719/// - `#[builder(default = expr)]` — specifies the default value expression
2720///
2721/// # Example
2722///
2723/// ```ignore
2724/// use sz_orm_macros::Builder;
2725///
2726/// #[derive(Builder)]
2727/// struct User {
2728///     id: i64,
2729///     name: String,
2730/// }
2731///
2732/// let user = User::builder()
2733///     .id(1)
2734///     .name("Alice".to_string())
2735///     .build()
2736///     .unwrap();
2737/// ```
2738#[proc_macro_derive(Builder, attributes(builder))]
2739pub fn derive_builder(input: TokenStream) -> TokenStream {
2740    let input = parse_macro_input!(input as syn::DeriveInput);
2741    derive::derive_builder_impl(input).into()
2742}
2743
2744// ---------------------------------------------------------------------------
2745// `#[derive(Entity)]` — auto-generate `impl Model for Struct`
2746// ---------------------------------------------------------------------------
2747
2748/// Derive macro: auto-generates an `sz_orm_core::Model` trait impl.
2749///
2750/// Requires the struct to have exactly one `#[column(primary_key)]` field;
2751/// that field's type becomes `Model::PrimaryKey`.
2752///
2753/// # Supported attributes
2754///
2755/// - `#[table(name = "...")]` — specifies the table name, defaults to the snake_case struct name
2756/// - `#[column(primary_key)]` — marks the primary key field (required, exactly one)
2757/// - `#[column(name = "...")]` — overrides the primary key column name (defaults to the field name)
2758///
2759/// # Example
2760///
2761/// ```ignore
2762/// use sz_orm_macros::Entity;
2763///
2764/// #[derive(Entity)]
2765/// #[table(name = "users")]
2766/// struct User {
2767///     #[column(primary_key)]
2768///     id: i64,
2769///     name: String,
2770/// }
2771///
2772/// assert_eq!(User::table_name(), "users");
2773/// assert_eq!(User::pk_name(), "id");
2774/// ```
2775#[proc_macro_derive(Entity, attributes(table, column))]
2776pub fn derive_entity(input: TokenStream) -> TokenStream {
2777    let input = parse_macro_input!(input as syn::DeriveInput);
2778    derive::derive_entity_impl(input).into()
2779}
2780
2781// ---------------------------------------------------------------------------
2782// `#[derive(FromQueryResult)]` — auto-generate `impl FromQueryResult for Struct`
2783// ---------------------------------------------------------------------------
2784
2785/// Derive macro: auto-generates an `sz_orm_core::FromQueryResult` trait impl.
2786///
2787/// Deserializes from a query result row (`HashMap<String, Value>`) into a struct instance.
2788/// `Option<T>` fields automatically return `None` when the column is missing or the value is NULL.
2789///
2790/// # Supported attributes
2791///
2792/// - `#[column(name = "...")]` — overrides the column name mapping (defaults to the field name)
2793///
2794/// # Example
2795///
2796/// ```ignore
2797/// use sz_orm_macros::FromQueryResult;
2798///
2799/// #[derive(FromQueryResult)]
2800/// struct UserRow {
2801///     id: i64,
2802///     name: String,
2803///     #[column(name = "user_email")]
2804///     email: Option<String>,
2805/// }
2806/// ```
2807#[proc_macro_derive(FromQueryResult, attributes(column))]
2808pub fn derive_from_query_result(input: TokenStream) -> TokenStream {
2809    let input = parse_macro_input!(input as syn::DeriveInput);
2810    derive::derive_from_query_result_impl(input).into()
2811}
2812
2813// ---------------------------------------------------------------------------
2814// `#[derive(ColumnEnum)]` — auto-generate column name enum (P2-2)
2815// ---------------------------------------------------------------------------
2816
2817/// Derive macro: auto-generates a `<StructName>Column` column-name enum from struct fields (P2-2).
2818///
2819/// Each field produces a variant (snake_case → CamelCase); `ColumnTrait::as_str()`
2820/// returns the database column name; `#[column(name = "...")]` can override the column name
2821/// (consistent with FromQueryResult). Also implements `std::fmt::Display`.
2822///
2823/// # Example
2824///
2825/// ```rust,ignore
2826/// use sz_orm_macros::ColumnEnum;
2827/// use sz_orm_core::ColumnTrait;
2828///
2829/// #[derive(ColumnEnum)]
2830/// struct User {
2831///     id: i64,
2832///     #[column(name = "user_name")]
2833///     name: String,
2834/// }
2835///
2836/// assert_eq!(UserColumn::Id.as_str(), "id");
2837/// assert_eq!(UserColumn::Name.as_str(), "user_name");
2838/// assert_eq!(UserColumn::Id.to_string(), "id");
2839/// ```
2840#[proc_macro_derive(ColumnEnum, attributes(column))]
2841pub fn derive_column_enum(input: TokenStream) -> TokenStream {
2842    let input = parse_macro_input!(input as syn::DeriveInput);
2843    derive::derive_column_enum_impl(input).into()
2844}
2845
2846// ---------------------------------------------------------------------------
2847// `#[derive(FromRow)]` — auto-generate `impl FromRow for Struct`
2848// ---------------------------------------------------------------------------
2849
2850/// Derive macro: auto-generates an `sz_orm_core::queryable::FromRow` trait impl.
2851///
2852/// Deserializes from a `HashMap<String, Value>` by column name into a struct instance.
2853/// Differs from `FromQueryResult` in that the error type is `QueryError` (includes column info),
2854/// suitable for low-level scenarios that need precise error localization.
2855///
2856/// # Supported attributes
2857///
2858/// - `#[column(name = "...")]` — overrides the column name mapping (defaults to the field name)
2859///
2860/// # Example
2861///
2862/// ```ignore
2863/// use sz_orm_macros::FromRow;
2864///
2865/// #[derive(FromRow)]
2866/// struct User {
2867///     id: i64,
2868///     name: String,
2869///     #[column(name = "user_email")]
2870///     email: Option<String>,
2871/// }
2872/// ```
2873#[proc_macro_derive(FromRow, attributes(column))]
2874pub fn derive_from_row(input: TokenStream) -> TokenStream {
2875    let input = parse_macro_input!(input as syn::DeriveInput);
2876    derive::derive_from_row_impl(input).into()
2877}
2878
2879// ---------------------------------------------------------------------------
2880// `#[derive(SqlType)]` — auto-generate `impl FromQueryResult + to_value()` for enums
2881// ---------------------------------------------------------------------------
2882
2883/// Derive macro: auto-generates an `sz_orm_core::FromQueryResult` trait impl
2884/// and a `to_value()` method for a Rust enum.
2885///
2886/// This is sz-orm's equivalent of SQLx's `#[derive(Type)]`:
2887/// it lets custom enums be used directly for query-result field mapping and query parameter binding.
2888///
2889/// # Supported attributes
2890///
2891/// - `#[sql_type(rename_all = "snake_case")]` — controls the serialization format of variant names
2892///   (snake_case / SCREAMING_SNAKE_CASE / camelCase / PascalCase / lowercase / UPPERCASE)
2893/// - `#[sql_type(rename = "...")]` (variant level) — overrides the serialization name of a single variant
2894///
2895/// # Example
2896///
2897/// ```ignore
2898/// use sz_orm_macros::SqlType;
2899///
2900/// #[derive(SqlType)]
2901/// enum Status {
2902///     Active,    // → "active"
2903///     Inactive,  // → "inactive"
2904/// }
2905///
2906/// let v = Status::Active.to_value();  // Value::String("active")
2907/// ```
2908#[proc_macro_derive(SqlType, attributes(sql_type))]
2909pub fn derive_sql_type(input: TokenStream) -> TokenStream {
2910    let input = parse_macro_input!(input as syn::DeriveInput);
2911    derive::derive_sql_type_impl(input).into()
2912}
2913
2914// ---------------------------------------------------------------------------
2915// `#[derive(Relation)]` — auto-generate `impl ModelExt` with relations()
2916// ---------------------------------------------------------------------------
2917
2918/// Derive macro: auto-generates an `sz_orm_core::model::ModelExt` trait impl,
2919/// populating the `relations()` map and eliminating hand-written relation boilerplate.
2920///
2921/// # Supported attributes
2922///
2923/// - `#[relation(has_many = "orders", fk = "user_id", pk = "id")]`
2924/// - `#[relation(belongs_to = "users", fk = "user_id", pk = "id")]`
2925/// - `#[relation(has_one = "profile", fk = "user_id", pk = "id")]`
2926/// - `#[relation(belongs_to_many = "roles", junction = "user_roles", fk = "user_id", other_key = "role_id", target = "roles", target_pk = "id")]`
2927/// - `#[relation(morph_many = "comments", morph_type = "commentable_type", morph_id = "commentable_id", morph_type_value = "Post")]`
2928/// - `#[relation(morph_to, morph_type = "commentable_type", morph_id = "commentable_id")]`
2929///
2930/// # Example
2931///
2932/// ```ignore
2933/// use sz_orm_macros::{Entity, Relation};
2934///
2935/// #[derive(Entity, Relation)]
2936/// #[table(name = "users")]
2937/// struct User {
2938///     #[column(primary_key)]
2939///     id: i64,
2940/// }
2941///
2942/// // Auto-generated:
2943/// // impl ModelExt for User {
2944/// //     fn relations() -> HashMap<&str, Relation> {
2945/// //         // contains the relations declared via #[relation]
2946/// //     }
2947/// // }
2948/// ```
2949#[proc_macro_derive(Relation, attributes(relation, table, column))]
2950pub fn derive_relation(input: TokenStream) -> TokenStream {
2951    let input = parse_macro_input!(input as syn::DeriveInput);
2952    derive::derive_relation_impl(input).into()
2953}
2954
2955/// `#[derive(RelationTrait)]` — auto-generates a `RelationTrait` impl (P-F-2, v2.1.0)
2956///
2957/// Generates a static `RelationDef` table + `impl RelationTrait` from `#[relation(...)]` attributes.
2958/// Shares attribute parsing with `#[derive(Relation)]`, but generates a zero-allocation static slice
2959/// instead of a `HashMap`.
2960///
2961/// # Example
2962///
2963/// ```ignore
2964/// #[derive(RelationTrait)]
2965/// #[relation(has_many = "Order", fk = "user_id", pk = "id")]
2966/// struct User { id: i64, name: String }
2967///
2968/// // Auto-generated:
2969/// // static RELATIONS: &[RelationDef] = &[RelationDef::new("Order", "users", "orders", "id", "user_id", HasMany)];
2970/// // impl RelationTrait for User { ... }
2971/// ```
2972#[proc_macro_derive(RelationTrait, attributes(relation, table, column))]
2973pub fn derive_relation_trait(input: TokenStream) -> TokenStream {
2974    let input = parse_macro_input!(input as syn::DeriveInput);
2975    derive::derive_relation_trait_impl(input).into()
2976}
2977
2978// ---------------------------------------------------------------------------
2979// v3.9.0:数据验证派生宏(data-validation feature 隔离)
2980// ---------------------------------------------------------------------------
2981
2982/// Derives a `Validate` trait impl.
2983///
2984/// Supported `#[validate(...)]` rules:
2985/// - `email` — email format validation
2986/// - `required` — non-empty validation
2987/// - `length(min=N, max=N)` — length range validation
2988/// - `range(min=N, max=N)` — numeric range validation
2989/// - `regex(pattern=r"...")` — regex match validation
2990/// - `contains(value="...")` — contains substring validation
2991/// - `does_not_contain(value="...")` — does not contain substring validation
2992/// - `if = "condition"` — conditional validation
2993///
2994/// # Example
2995///
2996/// ```ignore
2997/// #[derive(Validate)]
2998/// struct User {
2999///     #[validate(email)]
3000///     email: String,
3001///     #[validate(length(min=2, max=50))]
3002///     name: String,
3003///     #[validate(range(min=0, max=150))]
3004///     age: i64,
3005/// }
3006/// ```
3007#[cfg(feature = "data-validation")]
3008#[proc_macro_derive(Validate, attributes(validate))]
3009pub fn derive_validate(input: TokenStream) -> TokenStream {
3010    crate::derive_validate::derive_validate_impl(input)
3011}
3012
3013// ---------------------------------------------------------------------------
3014// v4.3.0 M3-T3:`#[derive(Governed)]` — 编译期数据治理(PII 标注强制)
3015// 通过 `compile-governance` feature(sz-orm-core)→ `governance-derive`(本包)启用。
3016// ---------------------------------------------------------------------------
3017
3018/// Derive macro: generates data-governance metadata for a model and **enforces compile-time** PII annotation compliance.
3019///
3020/// # Supported attributes
3021///
3022/// - `#[pii]` — marks the field as personally identifiable information (PII)
3023/// - `#[mask(strategy = "...")]` — declares a masking strategy (hash / partial / replace / encrypt)
3024///
3025/// # Compile-time enforcement rules
3026///
3027/// 1. A `#[pii]` field must also declare `#[mask(strategy = "...")]`, otherwise **compilation fails**
3028/// 2. The `mask` strategy must be within the whitelist (hash/partial/replace/encrypt), otherwise **compilation fails**
3029///
3030/// # Example
3031///
3032/// ```ignore
3033/// use sz_orm_core::governance::GovernedModel;
3034///
3035/// #[derive(Governed)]
3036/// struct User {
3037///     id: i64,
3038///     #[pii]
3039///     #[mask(strategy = "partial")]
3040///     email: String,
3041///     #[pii]
3042///     #[mask(strategy = "hash")]
3043///     phone: String,
3044///     name: String, // non-PII, no mask needed
3045/// }
3046///
3047/// assert_eq!(
3048///     User::pii_fields(),
3049///     vec![("email", "partial"), ("phone", "hash")]
3050/// );
3051/// ```
3052#[cfg(feature = "governance-derive")]
3053#[proc_macro_derive(Governed, attributes(pii, mask))]
3054pub fn derive_governed(input: TokenStream) -> TokenStream {
3055    let input = parse_macro_input!(input as syn::DeriveInput);
3056    let name = &input.ident;
3057
3058    const VALID_STRATEGIES: [&str; 4] = ["hash", "partial", "replace", "encrypt"];
3059
3060    let mut pii_fields: Vec<(String, String)> = Vec::new();
3061    let mut errors: Vec<syn::Error> = Vec::new();
3062
3063    if let syn::Data::Struct(data) = &input.data {
3064        for field in &data.fields {
3065            let Some(field_name) = field.ident.as_ref().map(|i| i.to_string()) else {
3066                continue;
3067            };
3068            let is_pii = field.attrs.iter().any(|a| a.path().is_ident("pii"));
3069
3070            // 解析 #[mask(strategy = "xxx")]
3071            let mut mask_strategy: Option<String> = None;
3072            for attr in &field.attrs {
3073                if !attr.path().is_ident("mask") {
3074                    continue;
3075                }
3076                let _ = attr.parse_nested_meta(|meta| {
3077                    if meta.path.is_ident("strategy") {
3078                        let lit: syn::LitStr = meta.value()?.parse()?;
3079                        mask_strategy = Some(lit.value());
3080                        Ok(())
3081                    } else {
3082                        Err(meta.error("unsupported #[mask] attribute, only 'strategy' is allowed"))
3083                    }
3084                });
3085            }
3086
3087            if is_pii {
3088                match mask_strategy {
3089                    Some(strategy) => {
3090                        if !VALID_STRATEGIES.contains(&strategy.as_str()) {
3091                            errors.push(syn::Error::new_spanned(
3092                                field,
3093                                format!(
3094                                    "invalid #[mask(strategy = \"{strategy}\")]: allowed strategies are {:?}",
3095                                    VALID_STRATEGIES
3096                                ),
3097                            ));
3098                        } else {
3099                            pii_fields.push((field_name, strategy));
3100                        }
3101                    }
3102                    None => errors.push(syn::Error::new_spanned(
3103                        field,
3104                        "#[pii] field must declare #[mask(strategy = \"...\")]",
3105                    )),
3106                }
3107            }
3108        }
3109    }
3110
3111    if !errors.is_empty() {
3112        // 每条错误独立转 compile_error!,合并为一个 TokenStream
3113        let err_tokens: proc_macro2::TokenStream =
3114            errors.iter().map(|e| e.to_compile_error()).collect();
3115        return err_tokens.into();
3116    }
3117
3118    let entries = pii_fields.iter().map(|(f, s)| {
3119        let f = f.as_str();
3120        let s = s.as_str();
3121        quote::quote!((#f, #s))
3122    });
3123
3124    quote::quote! {
3125        impl ::sz_orm_core::governance::GovernedModel for #name {
3126            fn pii_fields() -> Vec<(&'static str, &'static str)> {
3127                vec![#(#entries),*]
3128            }
3129        }
3130    }
3131    .into()
3132}
3133
3134// ---------------------------------------------------------------------------
3135// v4.3.0 M2:`#[detect_n_plus_one]` — N+1 静态检测标注宏(n1-lint feature)
3136// 分析函数体 AST,检测循环(for/while)内查询调用,编译期输出警告。
3137// 分析逻辑复用 sz-orm-n1-lint(与 CLI 批量扫描共用,避免重复实现)。
3138// ---------------------------------------------------------------------------
3139
3140/// Attribute macro: analyzes a function body, detects N+1 query patterns, and emits compile-time warnings (non-blocking).
3141///
3142/// Detection patterns (conservative whitelist to avoid false positives):
3143/// - `find_by_*` / `where_eq` / `query` and other query calls inside loop bodies → `query-in-loop` warning
3144/// - Query calls inside conditional branches within loops → `query-in-loop` warning
3145///
3146/// Warnings are emitted via `eprintln!` (`warning: [sz-orm-n1-lint] ...`); they do not block compilation;
3147/// the function is passed through unchanged (zero runtime impact).
3148///
3149/// # Example
3150///
3151/// ```ignore
3152/// #[detect_n_plus_one]
3153/// fn process_users(users: Vec<User>) {
3154///     for user in users {
3155///         let orders = Order::find_by_user(user.id); // ⚠️ compile-time warning: query-in-loop
3156///     }
3157/// }
3158/// ```
3159#[cfg(feature = "n1-lint")]
3160#[proc_macro_attribute]
3161pub fn detect_n_plus_one(_attr: TokenStream, item: TokenStream) -> TokenStream {
3162    let item_fn = parse_macro_input!(item as syn::ItemFn);
3163    let findings = sz_orm_n1_lint::analyze_fn(&item_fn);
3164    for f in &findings {
3165        eprintln!(
3166            "warning: [sz-orm-n1-lint] {} at line {}: {}",
3167            f.pattern.as_str(),
3168            f.line,
3169            f.message
3170        );
3171    }
3172    quote::quote!(#item_fn).into()
3173}
3174
3175/// `migrate!` 宏 — 编译时创建迁移并验证 SQL 语法
3176///
3177/// 对标 SQLx `migrate!` / Diesel `embed_migrations!`。
3178///
3179/// # 用法
3180///
3181/// ```ignore
3182/// use sz_orm_macros::migrate;
3183///
3184/// let m = migrate!("001", "create_users",
3185///     "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)",
3186///     "DROP TABLE users");
3187/// ```
3188///
3189/// 编译时验证 up/down SQL 语法,生成 `sz_orm_core::migration::Migration`。
3190#[proc_macro]
3191pub fn migrate(input: TokenStream) -> TokenStream {
3192    use syn::parse::Parser;
3193    let args = match syn::punctuated::Punctuated::<syn::LitStr, syn::Token![,]>::parse_terminated
3194        .parse2(input.into())
3195    {
3196        Ok(args) => args,
3197        Err(e) => return e.to_compile_error().into(),
3198    };
3199
3200    if args.len() != 4 {
3201        return syn::Error::new(
3202            proc_macro2::Span::call_site(),
3203            "migrate! expects exactly 4 arguments: version, name, sql_up, sql_down",
3204        )
3205        .to_compile_error()
3206        .into();
3207    }
3208
3209    let args_vec: Vec<_> = args.into_iter().collect();
3210    let sql_up = args_vec[2].value();
3211    let sql_down = args_vec[3].value();
3212
3213    if let Err(e) = validate_balanced_parens(&sql_up) {
3214        return syn::Error::new(args_vec[2].span(), format!("sql_up validation failed: {e}"))
3215            .to_compile_error()
3216            .into();
3217    }
3218    if let Err(e) = validate_string_literals_closed(&sql_up) {
3219        return syn::Error::new(args_vec[2].span(), format!("sql_up validation failed: {e}"))
3220            .to_compile_error()
3221            .into();
3222    }
3223    if !sql_down.is_empty() {
3224        if let Err(e) = validate_balanced_parens(&sql_down) {
3225            return syn::Error::new(
3226                args_vec[3].span(),
3227                format!("sql_down validation failed: {e}"),
3228            )
3229            .to_compile_error()
3230            .into();
3231        }
3232        if let Err(e) = validate_string_literals_closed(&sql_down) {
3233            return syn::Error::new(
3234                args_vec[3].span(),
3235                format!("sql_down validation failed: {e}"),
3236            )
3237            .to_compile_error()
3238            .into();
3239        }
3240    }
3241
3242    let version_lit = args_vec[0].clone();
3243    let name_lit = args_vec[1].clone();
3244    let up_lit = args_vec[2].clone();
3245    let down_lit = args_vec[3].clone();
3246
3247    quote::quote! {
3248        ::sz_orm_core::migration::Migration::new(
3249            #version_lit, #name_lit, #up_lit, #down_lit,
3250        )
3251    }
3252    .into()
3253}
3254
3255// ---------------------------------------------------------------------------
3256// Unit tests — cover helper functions used by both macros
3257// ---------------------------------------------------------------------------
3258
3259#[cfg(test)]
3260mod tests {
3261    use super::*;
3262
3263    // ---- strip_string_literal ----
3264
3265    #[test]
3266    fn test_strip_plain_double_quoted() {
3267        assert_eq!(strip_string_literal(r#""hello""#), Some("hello"));
3268    }
3269
3270    #[test]
3271    fn test_strip_raw_double_hash() {
3272        assert_eq!(strip_string_literal(r###"r#"hello"#"###), Some("hello"));
3273    }
3274
3275    #[test]
3276    fn test_strip_raw_double_no_hash() {
3277        assert_eq!(strip_string_literal(r#"r"hello""#), Some("hello"));
3278    }
3279
3280    #[test]
3281    fn test_strip_byte_string() {
3282        assert_eq!(strip_string_literal(r#"b"hello""#), Some("hello"));
3283        assert_eq!(strip_string_literal(r#"b'hello'"#), Some("hello"));
3284    }
3285
3286    #[test]
3287    fn test_strip_non_string_returns_none() {
3288        assert_eq!(strip_string_literal("123"), None);
3289        assert_eq!(strip_string_literal("foo"), None);
3290    }
3291
3292    // ---- validate_sql_content ----
3293
3294    #[test]
3295    fn test_validate_select_with_from_ok() {
3296        assert!(validate_sql_content("SELECT * FROM users", None).is_ok());
3297    }
3298
3299    #[test]
3300    fn test_validate_select_missing_from_fails() {
3301        assert!(validate_sql_content("SELECT * users", None).is_err());
3302    }
3303
3304    #[test]
3305    fn test_validate_insert_missing_into_fails() {
3306        assert!(validate_sql_content("INSERT INTO users VALUES (1)", None).is_ok());
3307        assert!(validate_sql_content("INSERT users VALUES (1)", None).is_err());
3308    }
3309
3310    #[test]
3311    fn test_validate_update_missing_set_fails() {
3312        assert!(validate_sql_content("UPDATE users SET name='a'", None).is_ok());
3313        assert!(validate_sql_content("UPDATE users name='a'", None).is_err());
3314    }
3315
3316    #[test]
3317    fn test_validate_delete_missing_from_fails() {
3318        assert!(validate_sql_content("DELETE FROM users WHERE id=1", None).is_ok());
3319        assert!(validate_sql_content("DELETE users WHERE id=1", None).is_err());
3320    }
3321
3322    #[test]
3323    fn test_validate_empty_sql_fails() {
3324        assert!(validate_sql_content("", None).is_err());
3325        assert!(validate_sql_content("   ", None).is_err());
3326    }
3327
3328    // ---- balanced parens ----
3329
3330    #[test]
3331    fn test_validate_balanced_parens_ok() {
3332        assert!(validate_balanced_parens("SELECT * FROM (SELECT * FROM t)").is_ok());
3333    }
3334
3335    #[test]
3336    fn test_validate_balanced_parens_unbalanced() {
3337        assert!(validate_balanced_parens("SELECT * FROM (t").is_err());
3338        assert!(validate_balanced_parens("SELECT * FROM t)").is_err());
3339    }
3340
3341    // ---- injection patterns ----
3342
3343    #[test]
3344    fn test_validate_no_injection_clean() {
3345        assert!(validate_no_injection("SELECT * FROM users WHERE id = 1").is_ok());
3346    }
3347
3348    #[test]
3349    fn test_validate_no_injection_drop_table() {
3350        assert!(validate_no_injection("'; DROP TABLE users; --").is_err());
3351    }
3352
3353    #[test]
3354    fn test_validate_no_injection_or_1_1() {
3355        // 编译期 SQL 已剥离外层引号,检测模式不再依赖引号字符。
3356        // "' OR '1'='1" 因引号分隔不再匹配 "or 1=1",故不再检测;
3357        // 但不含引号分隔的 "OR 1=1" 仍可被检测。
3358        assert!(validate_no_injection("' OR 1=1").is_err());
3359        assert!(validate_no_injection("WHERE id = 1 OR 1=1").is_err());
3360    }
3361
3362    #[test]
3363    fn test_validate_no_injection_drop_database() {
3364        assert!(validate_no_injection("SELECT x; DROP DATABASE db").is_err());
3365    }
3366
3367    #[test]
3368    fn test_validate_no_injection_information_schema() {
3369        assert!(validate_no_injection("SELECT * FROM information_schema.tables").is_err());
3370    }
3371
3372    #[test]
3373    fn test_validate_no_injection_xp_cmdshell() {
3374        assert!(validate_no_injection("EXEC xp_cmdshell 'dir'").is_err());
3375    }
3376
3377    #[test]
3378    fn test_validate_no_injection_union_select() {
3379        assert!(validate_no_injection("1 UNION SELECT * FROM users").is_err());
3380    }
3381
3382    #[test]
3383    fn test_validate_no_injection_comment_dashes() {
3384        assert!(validate_no_injection("SELECT * FROM users -- comment").is_err());
3385    }
3386
3387    #[test]
3388    fn test_validate_no_injection_block_comment() {
3389        assert!(validate_no_injection("SELECT /* x */ * FROM users").is_err());
3390    }
3391
3392    // ---- string literal closure ----
3393
3394    #[test]
3395    fn test_validate_string_literals_closed_ok() {
3396        assert!(validate_string_literals_closed("'hello' = 'world'").is_ok());
3397        assert!(validate_string_literals_closed(r#""foo" = "bar""#).is_ok());
3398    }
3399
3400    #[test]
3401    fn test_validate_string_literals_closed_unclosed_single() {
3402        assert!(validate_string_literals_closed("'hello").is_err());
3403    }
3404
3405    #[test]
3406    fn test_validate_string_literals_closed_unclosed_double() {
3407        assert!(validate_string_literals_closed(r#""hello"#).is_err());
3408    }
3409
3410    // ---- param count check ----
3411
3412    #[test]
3413    fn test_validate_param_count_match() {
3414        assert!(validate_sql_content("SELECT * FROM users WHERE id = ?", Some(1)).is_ok());
3415        assert!(
3416            validate_sql_content("SELECT * FROM users WHERE id = ? AND name = ?", Some(2)).is_ok()
3417        );
3418    }
3419
3420    #[test]
3421    fn test_validate_param_count_mismatch() {
3422        assert!(validate_sql_content("SELECT * FROM users WHERE id = ?", Some(2)).is_err());
3423        assert!(
3424            validate_sql_content("SELECT * FROM users WHERE id = ? AND name = ?", Some(1)).is_err()
3425        );
3426    }
3427
3428    // ---- db-verify feature: detect_db_kind ----
3429
3430    #[cfg(feature = "db-verify")]
3431    #[test]
3432    fn test_detect_db_kind_mysql() {
3433        assert_eq!(
3434            detect_db_kind("mysql://user:pass@host:3306/db").unwrap(),
3435            DbKind::MySql
3436        );
3437    }
3438
3439    #[cfg(feature = "db-verify")]
3440    #[test]
3441    fn test_detect_db_kind_postgres() {
3442        assert_eq!(
3443            detect_db_kind("postgres://user:pass@host:5432/db").unwrap(),
3444            DbKind::Postgres
3445        );
3446        assert_eq!(
3447            detect_db_kind("postgresql://user:pass@host:5432/db").unwrap(),
3448            DbKind::Postgres
3449        );
3450    }
3451
3452    #[cfg(feature = "db-verify")]
3453    #[test]
3454    fn test_detect_db_kind_sqlite() {
3455        assert_eq!(
3456            detect_db_kind("sqlite://path/to/db.db").unwrap(),
3457            DbKind::Sqlite
3458        );
3459        assert_eq!(detect_db_kind("sqlite::memory:").unwrap(), DbKind::Sqlite);
3460    }
3461
3462    #[cfg(feature = "db-verify")]
3463    #[test]
3464    fn test_detect_db_kind_oracle() {
3465        assert_eq!(
3466            detect_db_kind("oracle://sys:test123@127.0.0.1:1521/freepdb1.FALSE?sysdba=1").unwrap(),
3467            DbKind::Oracle
3468        );
3469        assert_eq!(
3470            detect_db_kind("oracle:sys:test123@127.0.0.1:1521/FREE").unwrap(),
3471            DbKind::Oracle
3472        );
3473    }
3474
3475    #[cfg(feature = "db-verify")]
3476    #[test]
3477    fn test_detect_db_kind_sqlserver() {
3478        assert_eq!(
3479            detect_db_kind("sqlserver://test:pass@host:1433/db").unwrap(),
3480            DbKind::SqlServer
3481        );
3482        assert_eq!(
3483            detect_db_kind("mssql://test:pass@host:1433/db").unwrap(),
3484            DbKind::SqlServer
3485        );
3486        assert_eq!(
3487            detect_db_kind("tds://test:pass@host:1433/db").unwrap(),
3488            DbKind::SqlServer
3489        );
3490    }
3491
3492    #[cfg(feature = "db-verify")]
3493    #[test]
3494    fn test_detect_db_kind_unsupported() {
3495        assert!(detect_db_kind("redis://user:pass@host/db").is_err());
3496        assert!(detect_db_kind("not-a-url").is_err());
3497    }
3498
3499    #[cfg(feature = "db-verify")]
3500    #[test]
3501    fn test_parse_oracle_dsn_basic() {
3502        let dsn = "oracle://sys:test123@127.0.0.1:1521/freepdb1.FALSE?sysdba=1";
3503        let p = parse_oracle_dsn(dsn).unwrap();
3504        assert_eq!(p.user, "sys");
3505        assert_eq!(p.password, "test123");
3506        assert_eq!(p.host, "127.0.0.1");
3507        assert_eq!(p.port, 1521);
3508        assert_eq!(p.service, "freepdb1.FALSE");
3509        assert!(p.sysdba);
3510    }
3511
3512    #[cfg(feature = "db-verify")]
3513    #[test]
3514    fn test_parse_oracle_dsn_default_port() {
3515        // 无端口号时默认 1521
3516        let dsn = "oracle://sys:test123@127.0.0.1/FREE";
3517        let p = parse_oracle_dsn(dsn).unwrap();
3518        assert_eq!(p.port, 1521);
3519        assert_eq!(p.service, "FREE");
3520        assert!(!p.sysdba);
3521    }
3522
3523    #[cfg(feature = "db-verify")]
3524    #[test]
3525    fn test_parse_sqlserver_dsn_basic() {
3526        let dsn =
3527            "sqlserver://test:JkbC2jsaWAYDe2Gz@sh-mssql-adrul9nm.sql.tencentcdb.com:22527/test";
3528        let p = parse_sqlserver_dsn(dsn).unwrap();
3529        assert_eq!(p.user, "test");
3530        assert_eq!(p.password, "JkbC2jsaWAYDe2Gz");
3531        assert_eq!(p.host, "sh-mssql-adrul9nm.sql.tencentcdb.com");
3532        assert_eq!(p.port, 22527);
3533        assert_eq!(p.database, "test");
3534    }
3535
3536    #[cfg(feature = "db-verify")]
3537    #[test]
3538    fn test_parse_sqlserver_dsn_default_port() {
3539        let dsn = "mssql://user:pass@host/db";
3540        let p = parse_sqlserver_dsn(dsn).unwrap();
3541        assert_eq!(p.port, 1433);
3542        assert_eq!(p.database, "db");
3543    }
3544
3545    // ---- schema! 宏 parse_create_table 测试 ----
3546
3547    #[test]
3548    fn test_parse_create_table_basic() {
3549        let sql = "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)";
3550        let (table, cols) = parse_create_table(sql).unwrap();
3551        assert_eq!(table, "users");
3552        assert_eq!(
3553            cols,
3554            vec![
3555                ("id".to_string(), "i32".to_string()),
3556                ("name".to_string(), "String".to_string())
3557            ]
3558        );
3559    }
3560
3561    #[test]
3562    fn test_parse_create_table_with_if_not_exists() {
3563        let sql = "CREATE TABLE IF NOT EXISTS `orders` (`id` BIGINT PRIMARY KEY, `total` DECIMAL(10,2) NOT NULL)";
3564        let (table, cols) = parse_create_table(sql).unwrap();
3565        assert_eq!(table, "orders");
3566        assert_eq!(
3567            cols,
3568            vec![
3569                ("id".to_string(), "i64".to_string()),
3570                ("total".to_string(), "f64".to_string())
3571            ]
3572        );
3573    }
3574
3575    #[test]
3576    fn test_parse_create_table_nullable() {
3577        let sql = "CREATE TABLE t (a INT NOT NULL, b INT)";
3578        let (_, cols) = parse_create_table(sql).unwrap();
3579        assert_eq!(cols[0], ("a".to_string(), "i32".to_string()));
3580        assert_eq!(cols[1], ("b".to_string(), "Option<i32>".to_string()));
3581    }
3582
3583    #[test]
3584    fn test_parse_create_table_skip_constraints() {
3585        let sql = "CREATE TABLE t (id INT PRIMARY KEY, name TEXT, PRIMARY KEY (id), CONSTRAINT fk1 FOREIGN KEY (x) REFERENCES y(id))";
3586        let (_, cols) = parse_create_table(sql).unwrap();
3587        assert_eq!(cols.len(), 2);
3588        assert_eq!(cols[0].0, "id");
3589        assert_eq!(cols[1].0, "name");
3590    }
3591
3592    #[test]
3593    fn test_parse_create_table_varchar_with_len() {
3594        let sql = "CREATE TABLE t (name VARCHAR(255) NOT NULL, code CHAR(10))";
3595        let (_, cols) = parse_create_table(sql).unwrap();
3596        assert_eq!(cols[0], ("name".to_string(), "String".to_string()));
3597        assert_eq!(cols[1], ("code".to_string(), "Option<String>".to_string()));
3598    }
3599
3600    #[test]
3601    fn test_sql_type_to_rust_mappings() {
3602        // 整数(按字节宽度严格映射,与 SQL 标准一致)
3603        assert_eq!(sql_type_to_rust("BIGINT", false), "i64");
3604        assert_eq!(sql_type_to_rust("INT8", false), "i64");
3605        assert_eq!(sql_type_to_rust("INT", false), "i32");
3606        assert_eq!(sql_type_to_rust("INTEGER", false), "i32");
3607        assert_eq!(sql_type_to_rust("INT4", false), "i32");
3608        assert_eq!(sql_type_to_rust("SERIAL", false), "i32");
3609        assert_eq!(sql_type_to_rust("SMALLINT", false), "i16");
3610        assert_eq!(sql_type_to_rust("INT2", false), "i16");
3611        assert_eq!(sql_type_to_rust("SMALLSERIAL", false), "i16");
3612        assert_eq!(sql_type_to_rust("TINYINT", false), "i8");
3613        // 浮点
3614        assert_eq!(sql_type_to_rust("FLOAT", false), "f32");
3615        assert_eq!(sql_type_to_rust("REAL", false), "f32");
3616        assert_eq!(sql_type_to_rust("FLOAT4", false), "f32");
3617        assert_eq!(sql_type_to_rust("DOUBLE", false), "f64");
3618        assert_eq!(sql_type_to_rust("DOUBLE PRECISION", false), "f64");
3619        assert_eq!(sql_type_to_rust("FLOAT8", false), "f64");
3620        assert_eq!(sql_type_to_rust("DECIMAL", false), "f64");
3621        assert_eq!(sql_type_to_rust("NUMERIC", false), "f64");
3622        // 布尔
3623        assert_eq!(sql_type_to_rust("BOOLEAN", false), "bool");
3624        assert_eq!(sql_type_to_rust("BOOL", false), "bool");
3625        // 字符串
3626        assert_eq!(sql_type_to_rust("VARCHAR", false), "String");
3627        assert_eq!(sql_type_to_rust("TEXT", false), "String");
3628        assert_eq!(sql_type_to_rust("CHAR", false), "String");
3629        assert_eq!(sql_type_to_rust("UUID", false), "String");
3630        assert_eq!(sql_type_to_rust("DATE", false), "String");
3631        assert_eq!(sql_type_to_rust("DATETIME", false), "String");
3632        assert_eq!(sql_type_to_rust("TIMESTAMP", false), "String");
3633        assert_eq!(sql_type_to_rust("JSON", false), "String");
3634        assert_eq!(sql_type_to_rust("JSONB", false), "String");
3635        // 二进制
3636        assert_eq!(sql_type_to_rust("BLOB", false), "Vec<u8>");
3637        assert_eq!(sql_type_to_rust("BYTEA", false), "Vec<u8>");
3638        assert_eq!(sql_type_to_rust("BINARY", false), "Vec<u8>");
3639        assert_eq!(sql_type_to_rust("VARBINARY", false), "Vec<u8>");
3640        // nullable
3641        assert_eq!(sql_type_to_rust("INT", true), "Option<i32>");
3642        assert_eq!(sql_type_to_rust("BIGINT", true), "Option<i64>");
3643        assert_eq!(sql_type_to_rust("VARCHAR", true), "Option<String>");
3644        assert_eq!(sql_type_to_rust("BLOB", true), "Option<Vec<u8>>");
3645        // unknown
3646        assert_eq!(sql_type_to_rust("UNKNOWNTYPE", false), "String");
3647    }
3648
3649    #[test]
3650    fn test_parse_create_table_error_no_create() {
3651        assert!(parse_create_table("SELECT * FROM users").is_err());
3652    }
3653
3654    #[test]
3655    fn test_parse_create_table_error_no_parens() {
3656        assert!(parse_create_table("CREATE TABLE foo").is_err());
3657    }
3658
3659    // -----------------------------------------------------------------------
3660    // Gap 1 测试:列名/表名提取
3661    // -----------------------------------------------------------------------
3662
3663    #[cfg(feature = "db-verify")]
3664    #[test]
3665    fn test_extract_tables_simple() {
3666        let tables = extract_tables("SELECT id, name FROM users WHERE id = ?");
3667        assert!(tables.contains(&"users".to_string()));
3668    }
3669
3670    #[cfg(feature = "db-verify")]
3671    #[test]
3672    fn test_extract_tables_multiple() {
3673        let tables = extract_tables(
3674            "SELECT u.id, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE u.id = ?",
3675        );
3676        assert!(tables.contains(&"users".to_string()));
3677        assert!(tables.contains(&"orders".to_string()));
3678    }
3679
3680    #[cfg(feature = "db-verify")]
3681    #[test]
3682    fn test_extract_columns_select_and_where() {
3683        let cols =
3684            extract_columns("SELECT id, name FROM users WHERE email = ? ORDER BY created_at");
3685        // id, name from SELECT; email from WHERE; created_at from ORDER BY
3686        assert!(cols.contains(&"id".to_string()));
3687        assert!(cols.contains(&"name".to_string()));
3688        assert!(cols.contains(&"email".to_string()));
3689        assert!(cols.contains(&"created_at".to_string()));
3690    }
3691
3692    #[cfg(feature = "db-verify")]
3693    #[test]
3694    fn test_extract_columns_skips_keywords() {
3695        let cols = extract_columns("SELECT COUNT(id), name FROM users WHERE status = ?");
3696        // COUNT is a function, should be skipped
3697        assert!(!cols.contains(&"count".to_string()));
3698        assert!(cols.contains(&"id".to_string()));
3699        assert!(cols.contains(&"name".to_string()));
3700        assert!(cols.contains(&"status".to_string()));
3701    }
3702
3703    #[cfg(feature = "db-verify")]
3704    #[test]
3705    fn test_is_sql_function() {
3706        assert!(is_sql_function("COUNT"));
3707        assert!(is_sql_function("now"));
3708        assert!(is_sql_function("COALESCE"));
3709        assert!(!is_sql_function("name"));
3710        assert!(!is_sql_function("user_id"));
3711    }
3712}