1use std::collections::HashMap;
41
42#[derive(Debug, Clone, Default)]
50pub struct PermissionContext {
51 pub user_id: Option<i64>,
53 pub tenant_id: Option<i64>,
55 pub dept_id: Option<i64>,
57 pub roles: Vec<String>,
59 pub permissions: Vec<String>,
61 pub extras: HashMap<String, String>,
63}
64
65impl PermissionContext {
66 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn with_user_id(mut self, user_id: i64) -> Self {
73 self.user_id = Some(user_id);
74 self
75 }
76
77 pub fn with_tenant_id(mut self, tenant_id: i64) -> Self {
79 self.tenant_id = Some(tenant_id);
80 self
81 }
82
83 pub fn with_dept_id(mut self, dept_id: i64) -> Self {
85 self.dept_id = Some(dept_id);
86 self
87 }
88
89 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
91 self.roles = roles;
92 self
93 }
94
95 pub fn with_permissions(mut self, perms: Vec<String>) -> Self {
97 self.permissions = perms;
98 self
99 }
100
101 pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
103 self.extras.insert(key.into(), value.into());
104 self
105 }
106
107 pub fn has_role(&self, role: &str) -> bool {
109 self.roles.iter().any(|r| r == role)
110 }
111
112 pub fn has_permission(&self, perm: &str) -> bool {
114 self.permissions.iter().any(|p| p == perm)
115 }
116
117 pub fn is_admin(&self) -> bool {
119 self.has_role("admin") || self.has_role("super_admin")
120 }
121}
122
123pub trait PermissionRule: Send + Sync {
137 fn name(&self) -> &'static str;
139
140 fn apply(&self, ctx: &PermissionContext) -> Result<Option<String>, PermissionError>;
142}
143
144#[derive(Debug)]
150pub enum PermissionError {
151 MissingContext {
153 field: &'static str,
155 },
156 ConfigError(String),
158 Forbidden(String),
160}
161
162impl std::fmt::Display for PermissionError {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 PermissionError::MissingContext { field } => {
166 write!(f, "Permission context missing field: `{}`", field)
167 }
168 PermissionError::ConfigError(msg) => {
169 write!(f, "Permission rule config error: {}", msg)
170 }
171 PermissionError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
172 }
173 }
174}
175
176impl std::error::Error for PermissionError {}
177
178pub type PermissionResult<T> = Result<T, PermissionError>;
180
181pub struct TenantIsolation {
200 pub field: &'static str,
202}
203
204impl TenantIsolation {
205 pub fn new(field: &'static str) -> Self {
207 Self { field }
208 }
209
210 pub fn default_field() -> Self {
212 Self::new("tenant_id")
213 }
214}
215
216impl PermissionRule for TenantIsolation {
217 fn name(&self) -> &'static str {
218 "TenantIsolation"
219 }
220
221 fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
222 if ctx.is_admin() {
224 return Ok(None);
225 }
226 match ctx.tenant_id {
227 Some(tid) => Ok(Some(format!("{} = {}", self.field, tid))),
228 None => Err(PermissionError::MissingContext { field: "tenant_id" }),
229 }
230 }
231}
232
233pub struct OwnerOnly {
252 pub field: &'static str,
254}
255
256impl OwnerOnly {
257 pub fn new(field: &'static str) -> Self {
259 Self { field }
260 }
261
262 pub fn default_field() -> Self {
264 Self::new("user_id")
265 }
266}
267
268impl PermissionRule for OwnerOnly {
269 fn name(&self) -> &'static str {
270 "OwnerOnly"
271 }
272
273 fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
274 if ctx.is_admin() {
276 return Ok(None);
277 }
278 match ctx.user_id {
279 Some(uid) => Ok(Some(format!("{} = {}", self.field, uid))),
280 None => Err(PermissionError::MissingContext { field: "user_id" }),
281 }
282 }
283}
284
285pub struct DepartmentScope {
304 pub field: &'static str,
306 pub include_sub_depts: Vec<i64>,
308}
309
310impl DepartmentScope {
311 pub fn new(field: &'static str) -> Self {
313 Self {
314 field,
315 include_sub_depts: Vec::new(),
316 }
317 }
318
319 pub fn default_field() -> Self {
321 Self::new("dept_id")
322 }
323
324 pub fn with_sub_depts(mut self, depts: Vec<i64>) -> Self {
326 self.include_sub_depts = depts;
327 self
328 }
329}
330
331impl PermissionRule for DepartmentScope {
332 fn name(&self) -> &'static str {
333 "DepartmentScope"
334 }
335
336 fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
337 if ctx.is_admin() {
338 return Ok(None);
339 }
340 match ctx.dept_id {
341 Some(did) => {
342 if self.include_sub_depts.is_empty() {
343 Ok(Some(format!("{} = {}", self.field, did)))
344 } else {
345 let mut all_depts = vec![did];
347 all_depts.extend(self.include_sub_depts.iter().copied());
348 let list = all_depts
349 .iter()
350 .map(|d| d.to_string())
351 .collect::<Vec<_>>()
352 .join(", ");
353 Ok(Some(format!("{} IN ({})", self.field, list)))
354 }
355 }
356 None => Err(PermissionError::MissingContext { field: "dept_id" }),
357 }
358 }
359}
360
361pub type ConditionGenerator = Box<dyn Fn(&PermissionContext) -> Option<String> + Send + Sync>;
367
368pub struct CustomCondition {
395 pub name_str: &'static str,
397 pub generator: ConditionGenerator,
399}
400
401impl CustomCondition {
402 pub fn new(
404 name: &'static str,
405 generator: impl Fn(&PermissionContext) -> Option<String> + Send + Sync + 'static,
406 ) -> Self {
407 Self {
408 name_str: name,
409 generator: Box::new(generator),
410 }
411 }
412}
413
414impl PermissionRule for CustomCondition {
415 fn name(&self) -> &'static str {
416 self.name_str
417 }
418
419 fn apply(&self, ctx: &PermissionContext) -> PermissionResult<Option<String>> {
420 Ok((self.generator)(ctx))
421 }
422}
423
424pub struct DataPermissionInterceptor {
455 rules: Vec<Box<dyn PermissionRule>>,
456}
457
458impl DataPermissionInterceptor {
459 pub fn new() -> Self {
461 Self { rules: Vec::new() }
462 }
463
464 pub fn register(&mut self, rule: Box<dyn PermissionRule>) {
466 self.rules.push(rule);
467 }
468
469 pub fn count(&self) -> usize {
471 self.rules.len()
472 }
473
474 pub fn names(&self) -> Vec<&'static str> {
476 self.rules.iter().map(|r| r.name()).collect()
477 }
478
479 pub fn collect_clauses(&self, ctx: &PermissionContext) -> PermissionResult<Vec<String>> {
481 let mut clauses = Vec::new();
482 for rule in &self.rules {
483 if let Some(clause) = rule.apply(ctx)? {
484 if !clause.trim().is_empty() {
485 clauses.push(clause);
486 }
487 }
488 }
489 Ok(clauses)
490 }
491
492 pub fn apply_to_select(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
497 let clauses = self.collect_clauses(ctx)?;
498 if clauses.is_empty() {
499 return Ok(sql.to_string());
500 }
501 Ok(append_where_clauses(sql, &clauses))
502 }
503
504 pub fn apply_to_update(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
506 self.apply_to_select(sql, ctx)
508 }
509
510 pub fn apply_to_delete(&self, sql: &str, ctx: &PermissionContext) -> PermissionResult<String> {
512 self.apply_to_select(sql, ctx)
513 }
514}
515
516impl Default for DataPermissionInterceptor {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522pub fn append_where_clauses(sql: &str, clauses: &[String]) -> String {
532 if clauses.is_empty() {
533 return sql.to_string();
534 }
535
536 let combined = clauses.join(" AND ");
537 let upper = sql.to_uppercase();
538
539 let where_pos = find_keyword(&upper, "WHERE");
541
542 let group_by_pos = find_keyword(&upper, "GROUP BY");
544 let order_by_pos = find_keyword(&upper, "ORDER BY");
545 let limit_pos = find_keyword(&upper, "LIMIT");
546 let having_pos = find_keyword(&upper, "HAVING");
547
548 let end_pos = [group_by_pos, order_by_pos, limit_pos, having_pos]
550 .iter()
551 .filter_map(|x| *x)
552 .min();
553
554 if let Some(wp) = where_pos {
555 let insert_pos = end_pos.unwrap_or(sql.len());
557 let before = &sql[..wp + 5]; let existing_clause = &sql[wp + 5..insert_pos];
559 let after = &sql[insert_pos..];
560
561 let trimmed_existing = existing_clause.trim();
563 if trimmed_existing.is_empty() {
564 format!("{} {}{}", before, combined, after)
565 } else {
566 format!(
567 "{} ({} ) AND ({}){}",
568 before, trimmed_existing, combined, after
569 )
570 }
571 } else {
572 let insert_pos = end_pos.unwrap_or(sql.len());
574 let before = &sql[..insert_pos];
575 let after = &sql[insert_pos..];
576 let trimmed = before.trim_end();
577 let sep = if trimmed.is_empty() { "" } else { " " };
578 format!("{}{}WHERE {}{}", trimmed, sep, combined, after)
579 }
580}
581
582fn find_keyword(sql: &str, keyword: &str) -> Option<usize> {
590 let upper_sql = sql.to_uppercase();
591 let kw_upper = keyword.to_uppercase();
592 let kw_len = kw_upper.len();
593 if kw_len == 0 || upper_sql.len() < kw_len {
594 return None;
595 }
596
597 let bytes = upper_sql.as_bytes();
598 let kw_bytes = kw_upper.as_bytes();
599
600 let mut i = 0;
601 let mut depth: i32 = 0; while i + kw_len <= bytes.len() {
603 let b = bytes[i];
604 if b == b'(' {
606 depth += 1;
607 i += 1;
608 continue;
609 }
610 if b == b')' {
611 if depth > 0 {
612 depth -= 1;
613 }
614 i += 1;
615 continue;
616 }
617 if depth == 0 && &bytes[i..i + kw_len] == kw_bytes {
619 let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
621 let next_idx = i + kw_len;
623 let next_ok = next_idx >= bytes.len()
624 || !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
625 if prev_ok && next_ok {
626 return Some(i);
627 }
628 }
629 i += 1;
630 }
631 None
632}
633
634#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
645 fn test_permission_context_builders() {
646 let ctx = PermissionContext::new()
647 .with_user_id(100)
648 .with_tenant_id(5)
649 .with_dept_id(3)
650 .with_roles(vec!["user".to_string()])
651 .with_permissions(vec!["read".to_string()])
652 .with_extra("region", "cn");
653
654 assert_eq!(ctx.user_id, Some(100));
655 assert_eq!(ctx.tenant_id, Some(5));
656 assert_eq!(ctx.dept_id, Some(3));
657 assert!(ctx.has_role("user"));
658 assert!(!ctx.has_role("admin"));
659 assert!(ctx.has_permission("read"));
660 assert_eq!(ctx.extras.get("region"), Some(&"cn".to_string()));
661 }
662
663 #[test]
664 fn test_permission_context_is_admin() {
665 let admin_ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
666 assert!(admin_ctx.is_admin());
667
668 let super_admin_ctx = PermissionContext::new().with_roles(vec!["super_admin".to_string()]);
669 assert!(super_admin_ctx.is_admin());
670
671 let user_ctx = PermissionContext::new().with_roles(vec!["user".to_string()]);
672 assert!(!user_ctx.is_admin());
673 }
674
675 #[test]
678 fn test_tenant_isolation_applies() {
679 let rule = TenantIsolation::default_field();
680 let ctx = PermissionContext::new().with_tenant_id(5);
681 let clause = rule.apply(&ctx).unwrap().unwrap();
682 assert_eq!(clause, "tenant_id = 5");
683 }
684
685 #[test]
686 fn test_tenant_isolation_skips_admin() {
687 let rule = TenantIsolation::default_field();
688 let ctx = PermissionContext::new()
689 .with_tenant_id(5)
690 .with_roles(vec!["admin".to_string()]);
691 let clause = rule.apply(&ctx).unwrap();
692 assert!(clause.is_none());
693 }
694
695 #[test]
696 fn test_tenant_isolation_missing_context() {
697 let rule = TenantIsolation::default_field();
698 let ctx = PermissionContext::new();
699 let result = rule.apply(&ctx);
700 assert!(matches!(
701 result,
702 Err(PermissionError::MissingContext { field }) if field == "tenant_id"
703 ));
704 }
705
706 #[test]
707 fn test_tenant_isolation_custom_field() {
708 let rule = TenantIsolation::new("org_id");
709 let ctx = PermissionContext::new().with_tenant_id(99);
710 let clause = rule.apply(&ctx).unwrap().unwrap();
711 assert_eq!(clause, "org_id = 99");
712 }
713
714 #[test]
717 fn test_owner_only_applies() {
718 let rule = OwnerOnly::default_field();
719 let ctx = PermissionContext::new().with_user_id(100);
720 let clause = rule.apply(&ctx).unwrap().unwrap();
721 assert_eq!(clause, "user_id = 100");
722 }
723
724 #[test]
725 fn test_owner_only_skips_admin() {
726 let rule = OwnerOnly::default_field();
727 let ctx = PermissionContext::new()
728 .with_user_id(100)
729 .with_roles(vec!["admin".to_string()]);
730 let clause = rule.apply(&ctx).unwrap();
731 assert!(clause.is_none());
732 }
733
734 #[test]
735 fn test_owner_only_missing_context() {
736 let rule = OwnerOnly::default_field();
737 let ctx = PermissionContext::new();
738 let result = rule.apply(&ctx);
739 assert!(matches!(
740 result,
741 Err(PermissionError::MissingContext { field }) if field == "user_id"
742 ));
743 }
744
745 #[test]
748 fn test_department_scope_simple() {
749 let rule = DepartmentScope::default_field();
750 let ctx = PermissionContext::new().with_dept_id(3);
751 let clause = rule.apply(&ctx).unwrap().unwrap();
752 assert_eq!(clause, "dept_id = 3");
753 }
754
755 #[test]
756 fn test_department_scope_with_sub_depts() {
757 let rule = DepartmentScope::default_field().with_sub_depts(vec![10, 11, 12]);
758 let ctx = PermissionContext::new().with_dept_id(3);
759 let clause = rule.apply(&ctx).unwrap().unwrap();
760 assert_eq!(clause, "dept_id IN (3, 10, 11, 12)");
761 }
762
763 #[test]
764 fn test_department_scope_skips_admin() {
765 let rule = DepartmentScope::default_field();
766 let ctx = PermissionContext::new()
767 .with_dept_id(3)
768 .with_roles(vec!["admin".to_string()]);
769 let clause = rule.apply(&ctx).unwrap();
770 assert!(clause.is_none());
771 }
772
773 #[test]
776 fn test_custom_condition_returns_clause() {
777 let rule = CustomCondition::new("draft_filter", |ctx| {
778 if ctx.is_admin() {
779 None
780 } else {
781 Some("status != 'draft'".to_string())
782 }
783 });
784 let ctx = PermissionContext::new().with_user_id(1);
785 let clause = rule.apply(&ctx).unwrap().unwrap();
786 assert_eq!(clause, "status != 'draft'");
787 }
788
789 #[test]
790 fn test_custom_condition_skips_admin() {
791 let rule = CustomCondition::new("draft_filter", |ctx| {
792 if ctx.is_admin() {
793 None
794 } else {
795 Some("status != 'draft'".to_string())
796 }
797 });
798 let ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
799 let clause = rule.apply(&ctx).unwrap();
800 assert!(clause.is_none());
801 }
802
803 #[test]
806 fn test_interceptor_no_rules_returns_original_sql() {
807 let interceptor = DataPermissionInterceptor::new();
808 let ctx = PermissionContext::new().with_user_id(1);
809 let sql = interceptor
810 .apply_to_select("SELECT * FROM users", &ctx)
811 .unwrap();
812 assert_eq!(sql, "SELECT * FROM users");
813 }
814
815 #[test]
816 fn test_interceptor_single_rule_no_where() {
817 let mut interceptor = DataPermissionInterceptor::new();
818 interceptor.register(Box::new(TenantIsolation::default_field()));
819
820 let ctx = PermissionContext::new().with_tenant_id(5);
821 let sql = interceptor
822 .apply_to_select("SELECT * FROM orders", &ctx)
823 .unwrap();
824 assert!(sql.contains("WHERE tenant_id = 5"));
825 }
826
827 #[test]
828 fn test_interceptor_multiple_rules_no_where() {
829 let mut interceptor = DataPermissionInterceptor::new();
830 interceptor.register(Box::new(TenantIsolation::default_field()));
831 interceptor.register(Box::new(OwnerOnly::default_field()));
832
833 let ctx = PermissionContext::new().with_tenant_id(5).with_user_id(100);
834 let sql = interceptor
835 .apply_to_select("SELECT * FROM orders", &ctx)
836 .unwrap();
837 assert!(sql.contains("tenant_id = 5"));
838 assert!(sql.contains("user_id = 100"));
839 assert!(sql.contains("AND"));
840 }
841
842 #[test]
843 fn test_interceptor_appends_to_existing_where() {
844 let mut interceptor = DataPermissionInterceptor::new();
845 interceptor.register(Box::new(TenantIsolation::default_field()));
846
847 let ctx = PermissionContext::new().with_tenant_id(5);
848 let sql = interceptor
849 .apply_to_select("SELECT * FROM orders WHERE status = 'active'", &ctx)
850 .unwrap();
851 assert!(sql.contains("status = 'active'"));
853 assert!(sql.contains("tenant_id = 5"));
854 assert!(sql.contains("AND"));
855 }
856
857 #[test]
858 fn test_interceptor_admin_skips_all_rules() {
859 let mut interceptor = DataPermissionInterceptor::new();
860 interceptor.register(Box::new(TenantIsolation::default_field()));
861 interceptor.register(Box::new(OwnerOnly::default_field()));
862
863 let ctx = PermissionContext::new()
864 .with_tenant_id(5)
865 .with_user_id(100)
866 .with_roles(vec!["admin".to_string()]);
867
868 let sql = interceptor
869 .apply_to_select("SELECT * FROM orders", &ctx)
870 .unwrap();
871 assert_eq!(sql, "SELECT * FROM orders");
873 }
874
875 #[test]
876 fn test_interceptor_apply_to_update() {
877 let mut interceptor = DataPermissionInterceptor::new();
878 interceptor.register(Box::new(TenantIsolation::default_field()));
879
880 let ctx = PermissionContext::new().with_tenant_id(5);
881 let sql = interceptor
882 .apply_to_update("UPDATE orders SET status = 'shipped' WHERE id = 1", &ctx)
883 .unwrap();
884 assert!(sql.contains("id = 1"));
885 assert!(sql.contains("tenant_id = 5"));
886 }
887
888 #[test]
889 fn test_interceptor_apply_to_delete() {
890 let mut interceptor = DataPermissionInterceptor::new();
891 interceptor.register(Box::new(OwnerOnly::default_field()));
892
893 let ctx = PermissionContext::new().with_user_id(100);
894 let sql = interceptor
895 .apply_to_delete("DELETE FROM orders WHERE id = 1", &ctx)
896 .unwrap();
897 assert!(sql.contains("id = 1"));
898 assert!(sql.contains("user_id = 100"));
899 }
900
901 #[test]
902 fn test_interceptor_count_and_names() {
903 let mut interceptor = DataPermissionInterceptor::new();
904 assert_eq!(interceptor.count(), 0);
905 interceptor.register(Box::new(TenantIsolation::default_field()));
906 interceptor.register(Box::new(OwnerOnly::default_field()));
907 assert_eq!(interceptor.count(), 2);
908 let names = interceptor.names();
909 assert!(names.contains(&"TenantIsolation"));
910 assert!(names.contains(&"OwnerOnly"));
911 }
912
913 #[test]
916 fn test_append_where_no_existing_where_no_clauses() {
917 let sql = append_where_clauses("SELECT * FROM users", &[]);
918 assert_eq!(sql, "SELECT * FROM users");
919 }
920
921 #[test]
922 fn test_append_where_no_existing_where_with_clauses() {
923 let sql = append_where_clauses("SELECT * FROM users", &["tenant_id = 5".to_string()]);
924 assert_eq!(sql, "SELECT * FROM users WHERE tenant_id = 5");
925 }
926
927 #[test]
928 fn test_append_where_existing_where_with_clauses() {
929 let sql = append_where_clauses(
930 "SELECT * FROM users WHERE id = 1",
931 &["tenant_id = 5".to_string()],
932 );
933 assert!(sql.contains("id = 1"));
934 assert!(sql.contains("tenant_id = 5"));
935 assert!(sql.contains("AND"));
936 }
937
938 #[test]
939 fn test_append_where_inserts_before_group_by() {
940 let sql = append_where_clauses(
941 "SELECT * FROM users GROUP BY dept_id",
942 &["tenant_id = 5".to_string()],
943 );
944 let where_idx = sql.to_uppercase().find("WHERE").unwrap();
946 let group_by_idx = sql.to_uppercase().find("GROUP BY").unwrap();
947 assert!(where_idx < group_by_idx);
948 }
949
950 #[test]
951 fn test_append_where_inserts_before_order_by() {
952 let sql = append_where_clauses(
953 "SELECT * FROM users ORDER BY id",
954 &["tenant_id = 5".to_string()],
955 );
956 let where_idx = sql.to_uppercase().find("WHERE").unwrap();
957 let order_by_idx = sql.to_uppercase().find("ORDER BY").unwrap();
958 assert!(where_idx < order_by_idx);
959 }
960
961 #[test]
962 fn test_append_where_inserts_before_limit() {
963 let sql = append_where_clauses(
964 "SELECT * FROM users LIMIT 10",
965 &["tenant_id = 5".to_string()],
966 );
967 let where_idx = sql.to_uppercase().find("WHERE").unwrap();
968 let limit_idx = sql.to_uppercase().find("LIMIT").unwrap();
969 assert!(where_idx < limit_idx);
970 }
971
972 #[test]
975 fn test_permission_error_display_missing_context() {
976 let e = PermissionError::MissingContext { field: "user_id" };
977 let s = format!("{}", e);
978 assert!(s.contains("user_id"));
979 assert!(s.contains("missing"));
980 }
981
982 #[test]
983 fn test_permission_error_display_config_error() {
984 let e = PermissionError::ConfigError("invalid rule".to_string());
985 let s = format!("{}", e);
986 assert!(s.contains("invalid rule"));
987 }
988
989 #[test]
990 fn test_permission_error_display_forbidden() {
991 let e = PermissionError::Forbidden("cross-tenant access".to_string());
992 let s = format!("{}", e);
993 assert!(s.contains("Forbidden"));
994 assert!(s.contains("cross-tenant access"));
995 }
996
997 #[test]
1000 fn test_find_keyword_basic() {
1001 assert_eq!(
1003 find_keyword("SELECT * FROM users WHERE id = 1", "WHERE"),
1004 Some(20)
1005 );
1006 }
1007
1008 #[test]
1009 fn test_find_keyword_not_found() {
1010 assert_eq!(find_keyword("SELECT * FROM users", "WHERE"), None);
1011 }
1012
1013 #[test]
1014 fn test_find_keyword_word_boundary() {
1015 assert_eq!(find_keyword("SELECT somewhere FROM t", "WHERE"), None);
1017 }
1018
1019 #[test]
1020 fn test_find_keyword_case_insensitive() {
1021 assert_eq!(
1023 find_keyword("select * from t where id = 1", "WHERE"),
1024 Some(16)
1025 );
1026 }
1027
1028 #[test]
1031 fn test_find_keyword_skips_subquery_where() {
1032 let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs WHERE level = 1)";
1034 let pos = find_keyword(sql, "WHERE").unwrap();
1035 assert_eq!(pos, 20);
1037 assert_eq!(&sql[pos..pos + 5], "WHERE");
1039 }
1040
1041 #[test]
1042 fn test_find_keyword_skips_subquery_limit() {
1043 let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)";
1045 assert_eq!(find_keyword(sql, "LIMIT"), None);
1047 }
1048
1049 #[test]
1050 fn test_find_keyword_finds_outer_limit() {
1051 let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5) LIMIT 10";
1053 let pos = find_keyword(sql, "LIMIT").unwrap();
1054 assert_eq!(&sql[pos..pos + 5], "LIMIT");
1056 assert!(pos > 50, "should match outer LIMIT, got pos={}", pos);
1058 }
1059
1060 #[test]
1061 fn test_find_keyword_skips_nested_parentheses() {
1062 let sql = "SELECT * FROM t WHERE id IN (SELECT id FROM (SELECT * FROM t2 WHERE x = 1) sub)";
1064 let pos = find_keyword(sql, "WHERE").unwrap();
1065 assert_eq!(&sql[pos..pos + 5], "WHERE");
1067 assert_eq!(pos, 16);
1068 }
1069
1070 #[test]
1071 fn test_find_keyword_unbalanced_parentheses_safe() {
1072 let sql = "SELECT * FROM t WHERE id = 1)";
1074 assert!(find_keyword(sql, "WHERE").is_some());
1076 }
1077
1078 #[test]
1081 fn test_interceptor_default_is_empty() {
1082 let i = DataPermissionInterceptor::default();
1083 assert_eq!(i.count(), 0);
1084 }
1085}