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)
580 }
581}
582
583fn find_keyword(sql: &str, keyword: &str) -> Option<usize> {
591 let upper_sql = sql.to_uppercase();
592 let kw_upper = keyword.to_uppercase();
593 let kw_len = kw_upper.len();
594 if kw_len == 0 || upper_sql.len() < kw_len {
595 return None;
596 }
597
598 let bytes = upper_sql.as_bytes();
599 let kw_bytes = kw_upper.as_bytes();
600
601 let mut i = 0;
602 let mut depth: i32 = 0; while i + kw_len <= bytes.len() {
604 let b = bytes[i];
605 if b == b'(' {
607 depth += 1;
608 i += 1;
609 continue;
610 }
611 if b == b')' {
612 if depth > 0 {
613 depth -= 1;
614 }
615 i += 1;
616 continue;
617 }
618 if depth == 0 && &bytes[i..i + kw_len] == kw_bytes {
620 let prev_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_';
622 let next_idx = i + kw_len;
624 let next_ok = next_idx >= bytes.len()
625 || !bytes[next_idx].is_ascii_alphanumeric() && bytes[next_idx] != b'_';
626 if prev_ok && next_ok {
627 return Some(i);
628 }
629 }
630 i += 1;
631 }
632 None
633}
634
635#[cfg(test)]
640mod tests {
641 use super::*;
642
643 #[test]
646 fn test_permission_context_builders() {
647 let ctx = PermissionContext::new()
648 .with_user_id(100)
649 .with_tenant_id(5)
650 .with_dept_id(3)
651 .with_roles(vec!["user".to_string()])
652 .with_permissions(vec!["read".to_string()])
653 .with_extra("region", "cn");
654
655 assert_eq!(ctx.user_id, Some(100));
656 assert_eq!(ctx.tenant_id, Some(5));
657 assert_eq!(ctx.dept_id, Some(3));
658 assert!(ctx.has_role("user"));
659 assert!(!ctx.has_role("admin"));
660 assert!(ctx.has_permission("read"));
661 assert_eq!(ctx.extras.get("region"), Some(&"cn".to_string()));
662 }
663
664 #[test]
665 fn test_permission_context_is_admin() {
666 let admin_ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
667 assert!(admin_ctx.is_admin());
668
669 let super_admin_ctx = PermissionContext::new().with_roles(vec!["super_admin".to_string()]);
670 assert!(super_admin_ctx.is_admin());
671
672 let user_ctx = PermissionContext::new().with_roles(vec!["user".to_string()]);
673 assert!(!user_ctx.is_admin());
674 }
675
676 #[test]
679 fn test_tenant_isolation_applies() {
680 let rule = TenantIsolation::default_field();
681 let ctx = PermissionContext::new().with_tenant_id(5);
682 let clause = rule.apply(&ctx).unwrap().unwrap();
683 assert_eq!(clause, "tenant_id = 5");
684 }
685
686 #[test]
687 fn test_tenant_isolation_skips_admin() {
688 let rule = TenantIsolation::default_field();
689 let ctx = PermissionContext::new()
690 .with_tenant_id(5)
691 .with_roles(vec!["admin".to_string()]);
692 let clause = rule.apply(&ctx).unwrap();
693 assert!(clause.is_none());
694 }
695
696 #[test]
697 fn test_tenant_isolation_missing_context() {
698 let rule = TenantIsolation::default_field();
699 let ctx = PermissionContext::new();
700 let result = rule.apply(&ctx);
701 assert!(matches!(
702 result,
703 Err(PermissionError::MissingContext { field }) if field == "tenant_id"
704 ));
705 }
706
707 #[test]
708 fn test_tenant_isolation_custom_field() {
709 let rule = TenantIsolation::new("org_id");
710 let ctx = PermissionContext::new().with_tenant_id(99);
711 let clause = rule.apply(&ctx).unwrap().unwrap();
712 assert_eq!(clause, "org_id = 99");
713 }
714
715 #[test]
718 fn test_owner_only_applies() {
719 let rule = OwnerOnly::default_field();
720 let ctx = PermissionContext::new().with_user_id(100);
721 let clause = rule.apply(&ctx).unwrap().unwrap();
722 assert_eq!(clause, "user_id = 100");
723 }
724
725 #[test]
726 fn test_owner_only_skips_admin() {
727 let rule = OwnerOnly::default_field();
728 let ctx = PermissionContext::new()
729 .with_user_id(100)
730 .with_roles(vec!["admin".to_string()]);
731 let clause = rule.apply(&ctx).unwrap();
732 assert!(clause.is_none());
733 }
734
735 #[test]
736 fn test_owner_only_missing_context() {
737 let rule = OwnerOnly::default_field();
738 let ctx = PermissionContext::new();
739 let result = rule.apply(&ctx);
740 assert!(matches!(
741 result,
742 Err(PermissionError::MissingContext { field }) if field == "user_id"
743 ));
744 }
745
746 #[test]
749 fn test_department_scope_simple() {
750 let rule = DepartmentScope::default_field();
751 let ctx = PermissionContext::new().with_dept_id(3);
752 let clause = rule.apply(&ctx).unwrap().unwrap();
753 assert_eq!(clause, "dept_id = 3");
754 }
755
756 #[test]
757 fn test_department_scope_with_sub_depts() {
758 let rule = DepartmentScope::default_field().with_sub_depts(vec![10, 11, 12]);
759 let ctx = PermissionContext::new().with_dept_id(3);
760 let clause = rule.apply(&ctx).unwrap().unwrap();
761 assert_eq!(clause, "dept_id IN (3, 10, 11, 12)");
762 }
763
764 #[test]
765 fn test_department_scope_skips_admin() {
766 let rule = DepartmentScope::default_field();
767 let ctx = PermissionContext::new()
768 .with_dept_id(3)
769 .with_roles(vec!["admin".to_string()]);
770 let clause = rule.apply(&ctx).unwrap();
771 assert!(clause.is_none());
772 }
773
774 #[test]
777 fn test_custom_condition_returns_clause() {
778 let rule = CustomCondition::new("draft_filter", |ctx| {
779 if ctx.is_admin() {
780 None
781 } else {
782 Some("status != 'draft'".to_string())
783 }
784 });
785 let ctx = PermissionContext::new().with_user_id(1);
786 let clause = rule.apply(&ctx).unwrap().unwrap();
787 assert_eq!(clause, "status != 'draft'");
788 }
789
790 #[test]
791 fn test_custom_condition_skips_admin() {
792 let rule = CustomCondition::new("draft_filter", |ctx| {
793 if ctx.is_admin() {
794 None
795 } else {
796 Some("status != 'draft'".to_string())
797 }
798 });
799 let ctx = PermissionContext::new().with_roles(vec!["admin".to_string()]);
800 let clause = rule.apply(&ctx).unwrap();
801 assert!(clause.is_none());
802 }
803
804 #[test]
807 fn test_interceptor_no_rules_returns_original_sql() {
808 let interceptor = DataPermissionInterceptor::new();
809 let ctx = PermissionContext::new().with_user_id(1);
810 let sql = interceptor
811 .apply_to_select("SELECT * FROM users", &ctx)
812 .unwrap();
813 assert_eq!(sql, "SELECT * FROM users");
814 }
815
816 #[test]
817 fn test_interceptor_single_rule_no_where() {
818 let mut interceptor = DataPermissionInterceptor::new();
819 interceptor.register(Box::new(TenantIsolation::default_field()));
820
821 let ctx = PermissionContext::new().with_tenant_id(5);
822 let sql = interceptor
823 .apply_to_select("SELECT * FROM orders", &ctx)
824 .unwrap();
825 assert!(sql.contains("WHERE tenant_id = 5"));
826 }
827
828 #[test]
829 fn test_interceptor_multiple_rules_no_where() {
830 let mut interceptor = DataPermissionInterceptor::new();
831 interceptor.register(Box::new(TenantIsolation::default_field()));
832 interceptor.register(Box::new(OwnerOnly::default_field()));
833
834 let ctx = PermissionContext::new().with_tenant_id(5).with_user_id(100);
835 let sql = interceptor
836 .apply_to_select("SELECT * FROM orders", &ctx)
837 .unwrap();
838 assert!(sql.contains("tenant_id = 5"));
839 assert!(sql.contains("user_id = 100"));
840 assert!(sql.contains("AND"));
841 }
842
843 #[test]
844 fn test_interceptor_appends_to_existing_where() {
845 let mut interceptor = DataPermissionInterceptor::new();
846 interceptor.register(Box::new(TenantIsolation::default_field()));
847
848 let ctx = PermissionContext::new().with_tenant_id(5);
849 let sql = interceptor
850 .apply_to_select("SELECT * FROM orders WHERE status = 'active'", &ctx)
851 .unwrap();
852 assert!(sql.contains("status = 'active'"));
854 assert!(sql.contains("tenant_id = 5"));
855 assert!(sql.contains("AND"));
856 }
857
858 #[test]
859 fn test_interceptor_admin_skips_all_rules() {
860 let mut interceptor = DataPermissionInterceptor::new();
861 interceptor.register(Box::new(TenantIsolation::default_field()));
862 interceptor.register(Box::new(OwnerOnly::default_field()));
863
864 let ctx = PermissionContext::new()
865 .with_tenant_id(5)
866 .with_user_id(100)
867 .with_roles(vec!["admin".to_string()]);
868
869 let sql = interceptor
870 .apply_to_select("SELECT * FROM orders", &ctx)
871 .unwrap();
872 assert_eq!(sql, "SELECT * FROM orders");
874 }
875
876 #[test]
877 fn test_interceptor_apply_to_update() {
878 let mut interceptor = DataPermissionInterceptor::new();
879 interceptor.register(Box::new(TenantIsolation::default_field()));
880
881 let ctx = PermissionContext::new().with_tenant_id(5);
882 let sql = interceptor
883 .apply_to_update("UPDATE orders SET status = 'shipped' WHERE id = 1", &ctx)
884 .unwrap();
885 assert!(sql.contains("id = 1"));
886 assert!(sql.contains("tenant_id = 5"));
887 }
888
889 #[test]
890 fn test_interceptor_apply_to_delete() {
891 let mut interceptor = DataPermissionInterceptor::new();
892 interceptor.register(Box::new(OwnerOnly::default_field()));
893
894 let ctx = PermissionContext::new().with_user_id(100);
895 let sql = interceptor
896 .apply_to_delete("DELETE FROM orders WHERE id = 1", &ctx)
897 .unwrap();
898 assert!(sql.contains("id = 1"));
899 assert!(sql.contains("user_id = 100"));
900 }
901
902 #[test]
903 fn test_interceptor_count_and_names() {
904 let mut interceptor = DataPermissionInterceptor::new();
905 assert_eq!(interceptor.count(), 0);
906 interceptor.register(Box::new(TenantIsolation::default_field()));
907 interceptor.register(Box::new(OwnerOnly::default_field()));
908 assert_eq!(interceptor.count(), 2);
909 let names = interceptor.names();
910 assert!(names.contains(&"TenantIsolation"));
911 assert!(names.contains(&"OwnerOnly"));
912 }
913
914 #[test]
917 fn test_append_where_no_existing_where_no_clauses() {
918 let sql = append_where_clauses("SELECT * FROM users", &[]);
919 assert_eq!(sql, "SELECT * FROM users");
920 }
921
922 #[test]
923 fn test_append_where_no_existing_where_with_clauses() {
924 let sql = append_where_clauses("SELECT * FROM users", &["tenant_id = 5".to_string()]);
925 assert_eq!(sql, "SELECT * FROM users WHERE tenant_id = 5");
926 }
927
928 #[test]
929 fn test_append_where_existing_where_with_clauses() {
930 let sql = append_where_clauses(
931 "SELECT * FROM users WHERE id = 1",
932 &["tenant_id = 5".to_string()],
933 );
934 assert!(sql.contains("id = 1"));
935 assert!(sql.contains("tenant_id = 5"));
936 assert!(sql.contains("AND"));
937 }
938
939 #[test]
940 fn test_append_where_inserts_before_group_by() {
941 let sql = append_where_clauses(
942 "SELECT * FROM users GROUP BY dept_id",
943 &["tenant_id = 5".to_string()],
944 );
945 let where_idx = sql.to_uppercase().find("WHERE").unwrap();
947 let group_by_idx = sql.to_uppercase().find("GROUP BY").unwrap();
948 assert!(where_idx < group_by_idx);
949 }
950
951 #[test]
952 fn test_append_where_inserts_before_order_by() {
953 let sql = append_where_clauses(
954 "SELECT * FROM users ORDER BY id",
955 &["tenant_id = 5".to_string()],
956 );
957 let where_idx = sql.to_uppercase().find("WHERE").unwrap();
958 let order_by_idx = sql.to_uppercase().find("ORDER BY").unwrap();
959 assert!(where_idx < order_by_idx);
960 }
961
962 #[test]
963 fn test_append_where_inserts_before_limit() {
964 let sql = append_where_clauses(
965 "SELECT * FROM users LIMIT 10",
966 &["tenant_id = 5".to_string()],
967 );
968 let where_idx = sql.to_uppercase().find("WHERE").unwrap();
969 let limit_idx = sql.to_uppercase().find("LIMIT").unwrap();
970 assert!(where_idx < limit_idx);
971 }
972
973 #[test]
976 fn test_permission_error_display_missing_context() {
977 let e = PermissionError::MissingContext { field: "user_id" };
978 let s = format!("{}", e);
979 assert!(s.contains("user_id"));
980 assert!(s.contains("missing"));
981 }
982
983 #[test]
984 fn test_permission_error_display_config_error() {
985 let e = PermissionError::ConfigError("invalid rule".to_string());
986 let s = format!("{}", e);
987 assert!(s.contains("invalid rule"));
988 }
989
990 #[test]
991 fn test_permission_error_display_forbidden() {
992 let e = PermissionError::Forbidden("cross-tenant access".to_string());
993 let s = format!("{}", e);
994 assert!(s.contains("Forbidden"));
995 assert!(s.contains("cross-tenant access"));
996 }
997
998 #[test]
1001 fn test_find_keyword_basic() {
1002 assert_eq!(
1004 find_keyword("SELECT * FROM users WHERE id = 1", "WHERE"),
1005 Some(20)
1006 );
1007 }
1008
1009 #[test]
1010 fn test_find_keyword_not_found() {
1011 assert_eq!(find_keyword("SELECT * FROM users", "WHERE"), None);
1012 }
1013
1014 #[test]
1015 fn test_find_keyword_word_boundary() {
1016 assert_eq!(find_keyword("SELECT somewhere FROM t", "WHERE"), None);
1018 }
1019
1020 #[test]
1021 fn test_find_keyword_case_insensitive() {
1022 assert_eq!(
1024 find_keyword("select * from t where id = 1", "WHERE"),
1025 Some(16)
1026 );
1027 }
1028
1029 #[test]
1032 fn test_find_keyword_skips_subquery_where() {
1033 let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs WHERE level = 1)";
1035 let pos = find_keyword(sql, "WHERE").unwrap();
1036 assert_eq!(pos, 20);
1038 assert_eq!(&sql[pos..pos + 5], "WHERE");
1040 }
1041
1042 #[test]
1043 fn test_find_keyword_skips_subquery_limit() {
1044 let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5)";
1046 assert_eq!(find_keyword(sql, "LIMIT"), None);
1048 }
1049
1050 #[test]
1051 fn test_find_keyword_finds_outer_limit() {
1052 let sql = "SELECT * FROM users WHERE id IN (SELECT id FROM logs LIMIT 5) LIMIT 10";
1054 let pos = find_keyword(sql, "LIMIT").unwrap();
1055 assert_eq!(&sql[pos..pos + 5], "LIMIT");
1057 assert!(pos > 50, "should match outer LIMIT, got pos={}", pos);
1059 }
1060
1061 #[test]
1062 fn test_find_keyword_skips_nested_parentheses() {
1063 let sql = "SELECT * FROM t WHERE id IN (SELECT id FROM (SELECT * FROM t2 WHERE x = 1) sub)";
1065 let pos = find_keyword(sql, "WHERE").unwrap();
1066 assert_eq!(&sql[pos..pos + 5], "WHERE");
1068 assert_eq!(pos, 16);
1069 }
1070
1071 #[test]
1072 fn test_find_keyword_unbalanced_parentheses_safe() {
1073 let sql = "SELECT * FROM t WHERE id = 1)";
1075 assert!(find_keyword(sql, "WHERE").is_some());
1077 }
1078
1079 #[test]
1082 fn test_interceptor_default_is_empty() {
1083 let i = DataPermissionInterceptor::default();
1084 assert_eq!(i.count(), 0);
1085 }
1086}