1#![allow(linker_messages)]
35extern 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
63use proc_macro2::TokenStream as TokenStream2;
65use quote::quote;
66use syn::parse_macro_input;
67
68mod derive;
70
71#[cfg(feature = "data-validation")]
73mod derive_validate;
74
75#[cfg(any(feature = "typed-dsl", feature = "custom-diagnostic"))]
77mod diagnostic;
78
79#[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#[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#[proc_macro]
179pub fn sql_string(input: TokenStream) -> TokenStream {
180 let mut tokens = input.into_iter().peekable();
181
182 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 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 let mut expected_params = None;
217 if tokens.peek().is_some() {
218 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 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 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 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 if let Err(err_msg) = validate_sql_content(sql_content, expected_params) {
285 return compile_error(Span::call_site(), &err_msg);
286 }
287
288 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
295fn 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 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 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 let injection_patterns: &[&str] = &[
402 "drop table",
404 "drop database",
405 "; drop",
406 "or 1=1",
408 "or 1 = 1",
409 "union select",
410 "union all select",
411 "--",
413 "/*",
414 "*/",
415 "xp_cmdshell",
417 "sp_executesql",
418 "exec(",
419 "execute(",
420 "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#[proc_macro]
469pub fn query(input: TokenStream) -> TokenStream {
470 let mut tokens = input.into_iter().peekable();
471
472 let type_param: Option<TokenStream2> = match tokens.peek() {
475 Some(TokenTree::Ident(_)) | Some(TokenTree::Punct(_)) => {
476 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 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 match tokens.peek() {
495 Some(TokenTree::Punct(p)) if p.as_char() == ',' => {
496 tokens.next(); let ts: proc_macro::TokenStream = ty_tokens.into_iter().collect();
498 Some(TokenStream2::from(ts))
499 }
500 _ => None, }
502 }
503 _ => None,
504 };
505
506 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 if let Err(err_msg) = validate_sql_content(sql_content, None) {
543 return compile_error(Span::call_site(), &err_msg);
544 }
545
546 #[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 Some("1") => match verify_with_real_db(sql_content) {
552 Ok(cols) => {
553 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 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 let escaped = sql_content.escape_default().to_string();
584 let base = if let Some(ref ty) = type_param {
585 format!(
587 "::sz_orm_core::queryable::QueryAs::<{}>::new(\"{}\")",
588 ty, escaped
589 )
590 } else {
591 format!("::sz_orm_core::queryable::Query::new(\"{}\")", escaped)
593 };
594 #[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
609fn 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#[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 let sql_no_placeholders = replace_placeholders_with_null(sql);
640
641 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 DbKind::Oracle => format!("EXPLAIN PLAN FOR {}", sql_no_placeholders),
647 DbKind::SqlServer => sql_no_placeholders,
649 };
650
651 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 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 verify_sqlite(&dsn, &explain_sql).await?;
664 }
665 verify_columns(&dsn, db_kind, sql).await?;
667 fetch_column_types(&dsn, db_kind, sql).await
670 });
671 }
672
673 if let DbKind::Oracle = db_kind {
675 verify_oracle(&dsn, &explain_sql).map(|_| Vec::new())
676 } else {
677 verify_sqlserver(&dsn, &explain_sql).map(|_| Vec::new())
679 }
680}
681
682#[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#[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 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#[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#[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#[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#[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 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#[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#[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 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#[cfg(feature = "db-verify")]
1002async fn verify_columns(dsn: &str, db_kind: DbKind, sql: &str) -> Result<(), String> {
1003 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#[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 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 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 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 let table_word = part
1105 .split_whitespace()
1106 .next()
1107 .unwrap_or(part)
1108 .trim_end_matches([',', ';']);
1109 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#[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 let mut collect_from_segment = |segment: &str| {
1141 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 if word.chars().all(|c| c.is_ascii_digit()) {
1216 continue;
1217 }
1218 let pos = segment.find(word).unwrap_or(0);
1221 if pos > 0 && segment.chars().nth(pos - 1) == Some('.') {
1222 continue;
1223 }
1224 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 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 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 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 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 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 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 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#[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 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 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#[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#[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#[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#[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#[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#[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 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 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#[cfg(feature = "db-verify")]
1731struct SqlServerDsn {
1732 user: String,
1733 password: String,
1734 host: String,
1735 port: u16,
1736 database: String,
1737}
1738
1739#[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
1777fn compile_error(span: Span, msg: &str) -> TokenStream {
1783 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#[proc_macro]
1834pub fn typed_query(input: TokenStream) -> TokenStream {
1835 let tokens: Vec<TokenTree> = input.into_iter().collect();
1836
1837 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 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
1865fn parse_table_decl(tokens: &[TokenTree]) -> TokenStream {
1867 let mut idx = 0;
1869
1870 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 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 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 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 let table_ident = proc_macro2::Ident::new(&table_name, Span::call_site().into());
1916 let table_name_lit = table_name.as_str();
1917
1918 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 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 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
1970fn 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 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 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 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
2033fn parse_typed_select(tokens: &[TokenTree]) -> TokenStream {
2037 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 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 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 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 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 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 let mut ts = TokenStream::new();
2156 let lit = Literal::string(&sql);
2157 ts.extend([TokenTree::Literal(lit)]);
2158 ts
2159}
2160
2161#[proc_macro]
2191pub fn query_as(input: TokenStream) -> TokenStream {
2213 let mut tokens = input.into_iter().peekable();
2214
2215 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 record_type.push_str("::");
2225 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 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 if let Err(err_msg) = validate_sql_content(sql_content, None) {
2272 return compile_error(Span::call_site(), &err_msg);
2273 }
2274
2275 #[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 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 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 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 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 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 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
2418fn parse_create_table(sql: &str) -> Result<(String, Vec<(String, String)>), String> {
2426 let trimmed = sql.trim();
2427 let upper = trimmed.to_uppercase();
2428
2429 if !upper.starts_with("CREATE TABLE") {
2431 return Err("schema! expects a CREATE TABLE statement".to_string());
2432 }
2433
2434 let mut rest = &trimmed["CREATE TABLE".len()..];
2436
2437 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 let (table_name, after_name) = parse_identifier(rest)?;
2447 let rest = after_name.trim_start();
2448
2449 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 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 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 let (col_name, after_col) = parse_identifier(def)?;
2486 let rest = after_col.trim_start();
2487
2488 let (sql_type, after_type) = parse_type_token(rest)?;
2490 let rest = after_type.trim();
2491
2492 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
2503fn 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
2540fn 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 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
2567fn 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
2599fn 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 "BIGINT" | "INT8" => "i64",
2609 "INT" | "INTEGER" | "INT4" | "SERIAL" => "i32",
2611 "SMALLINT" | "INT2" | "SMALLSERIAL" => "i16",
2613 "TINYINT" => "i8",
2615 "FLOAT" | "REAL" | "FLOAT4" => "f32",
2617 "DOUBLE" | "DOUBLE PRECISION" | "FLOAT8" | "DECIMAL" | "NUMERIC" => "f64",
2619 "BOOLEAN" | "BOOL" => "bool",
2621 "BLOB" | "BYTEA" | "BINARY" | "VARBINARY" => "Vec<u8>",
2623 "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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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#[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#[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#[cfg(test)]
3260mod tests {
3261 use super::*;
3262
3263 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 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 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 assert_eq!(sql_type_to_rust("BOOLEAN", false), "bool");
3624 assert_eq!(sql_type_to_rust("BOOL", false), "bool");
3625 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 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 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 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 #[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 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 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}