1use serde::{Deserialize, Serialize};
65use smallvec::SmallVec;
66use std::borrow::Cow;
67use tracing::debug;
68
69pub type ValueList = Vec<FilterValue>;
84
85pub type SmallValueList = SmallVec<[FilterValue; 8]>;
88
89pub type LargeValueList = SmallVec<[FilterValue; 32]>;
92
93pub type FieldName = Cow<'static, str>;
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148#[serde(untagged)]
149pub enum FilterValue {
150 Null,
152 Bool(bool),
154 Int(i64),
156 Float(f64),
158 String(String),
160 Json(serde_json::Value),
162 List(Vec<FilterValue>),
164}
165
166impl FilterValue {
167 pub fn is_null(&self) -> bool {
169 matches!(self, Self::Null)
170 }
171}
172
173impl From<bool> for FilterValue {
174 fn from(v: bool) -> Self {
175 Self::Bool(v)
176 }
177}
178
179impl From<i32> for FilterValue {
180 fn from(v: i32) -> Self {
181 Self::Int(v as i64)
182 }
183}
184
185impl From<i64> for FilterValue {
186 fn from(v: i64) -> Self {
187 Self::Int(v)
188 }
189}
190
191impl From<f64> for FilterValue {
192 fn from(v: f64) -> Self {
193 Self::Float(v)
194 }
195}
196
197impl From<String> for FilterValue {
198 fn from(v: String) -> Self {
199 Self::String(v)
200 }
201}
202
203impl From<&str> for FilterValue {
204 fn from(v: &str) -> Self {
205 Self::String(v.to_string())
206 }
207}
208
209impl<T: Into<FilterValue>> From<Vec<T>> for FilterValue {
210 fn from(v: Vec<T>) -> Self {
211 Self::List(v.into_iter().map(Into::into).collect())
212 }
213}
214
215impl<T: Into<FilterValue>> From<Option<T>> for FilterValue {
216 fn from(v: Option<T>) -> Self {
217 match v {
218 Some(v) => v.into(),
219 None => Self::Null,
220 }
221 }
222}
223
224impl From<i8> for FilterValue {
230 fn from(v: i8) -> Self {
231 Self::Int(v as i64)
232 }
233}
234impl From<i16> for FilterValue {
235 fn from(v: i16) -> Self {
236 Self::Int(v as i64)
237 }
238}
239impl From<u8> for FilterValue {
240 fn from(v: u8) -> Self {
241 Self::Int(v as i64)
242 }
243}
244impl From<u16> for FilterValue {
245 fn from(v: u16) -> Self {
246 Self::Int(v as i64)
247 }
248}
249impl From<u32> for FilterValue {
250 fn from(v: u32) -> Self {
251 Self::Int(v as i64)
252 }
253}
254impl From<u64> for FilterValue {
260 fn from(v: u64) -> Self {
261 let v = i64::try_from(v).expect(
262 "u64 value exceeds i64::MAX; cast explicitly to i64 or use FilterValue::String",
263 );
264 Self::Int(v)
265 }
266}
267
268impl From<f32> for FilterValue {
269 fn from(v: f32) -> Self {
270 Self::Float(f64::from(v))
271 }
272}
273
274impl From<chrono::DateTime<chrono::Utc>> for FilterValue {
286 fn from(v: chrono::DateTime<chrono::Utc>) -> Self {
287 Self::String(v.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
289 }
290}
291impl From<chrono::NaiveDateTime> for FilterValue {
292 fn from(v: chrono::NaiveDateTime) -> Self {
293 Self::String(v.format("%Y-%m-%dT%H:%M:%S%.6f").to_string())
296 }
297}
298impl From<chrono::NaiveDate> for FilterValue {
299 fn from(v: chrono::NaiveDate) -> Self {
300 Self::String(v.format("%Y-%m-%d").to_string())
301 }
302}
303impl From<chrono::NaiveTime> for FilterValue {
304 fn from(v: chrono::NaiveTime) -> Self {
305 Self::String(v.format("%H:%M:%S%.6f").to_string())
306 }
307}
308
309impl From<uuid::Uuid> for FilterValue {
310 fn from(v: uuid::Uuid) -> Self {
311 Self::String(v.to_string())
312 }
313}
314
315impl From<rust_decimal::Decimal> for FilterValue {
316 fn from(v: rust_decimal::Decimal) -> Self {
317 Self::String(v.to_string())
318 }
319}
320
321impl From<serde_json::Value> for FilterValue {
322 fn from(v: serde_json::Value) -> Self {
323 Self::Json(v)
324 }
325}
326
327pub trait ToFilterValue {
349 fn to_filter_value(&self) -> FilterValue;
351}
352
353impl ToFilterValue for i8 {
354 fn to_filter_value(&self) -> FilterValue {
355 FilterValue::Int(*self as i64)
356 }
357}
358impl ToFilterValue for i16 {
359 fn to_filter_value(&self) -> FilterValue {
360 FilterValue::Int(*self as i64)
361 }
362}
363impl ToFilterValue for i32 {
364 fn to_filter_value(&self) -> FilterValue {
365 FilterValue::Int(*self as i64)
366 }
367}
368impl ToFilterValue for i64 {
369 fn to_filter_value(&self) -> FilterValue {
370 FilterValue::Int(*self)
371 }
372}
373impl ToFilterValue for u8 {
374 fn to_filter_value(&self) -> FilterValue {
375 FilterValue::Int(*self as i64)
376 }
377}
378impl ToFilterValue for u16 {
379 fn to_filter_value(&self) -> FilterValue {
380 FilterValue::Int(*self as i64)
381 }
382}
383impl ToFilterValue for u32 {
384 fn to_filter_value(&self) -> FilterValue {
385 FilterValue::Int(*self as i64)
386 }
387}
388impl ToFilterValue for f32 {
389 fn to_filter_value(&self) -> FilterValue {
390 FilterValue::Float(f64::from(*self))
391 }
392}
393impl ToFilterValue for f64 {
394 fn to_filter_value(&self) -> FilterValue {
395 FilterValue::Float(*self)
396 }
397}
398impl ToFilterValue for bool {
399 fn to_filter_value(&self) -> FilterValue {
400 FilterValue::Bool(*self)
401 }
402}
403impl ToFilterValue for String {
404 fn to_filter_value(&self) -> FilterValue {
405 FilterValue::String(self.clone())
406 }
407}
408impl ToFilterValue for str {
409 fn to_filter_value(&self) -> FilterValue {
410 FilterValue::String(self.to_string())
411 }
412}
413impl ToFilterValue for uuid::Uuid {
414 fn to_filter_value(&self) -> FilterValue {
415 FilterValue::String(self.to_string())
416 }
417}
418impl ToFilterValue for rust_decimal::Decimal {
419 fn to_filter_value(&self) -> FilterValue {
420 FilterValue::String(self.to_string())
421 }
422}
423impl ToFilterValue for chrono::DateTime<chrono::Utc> {
424 fn to_filter_value(&self) -> FilterValue {
425 FilterValue::String(self.to_rfc3339_opts(chrono::SecondsFormat::Micros, true))
427 }
428}
429impl ToFilterValue for chrono::NaiveDateTime {
430 fn to_filter_value(&self) -> FilterValue {
431 FilterValue::String(self.format("%Y-%m-%dT%H:%M:%S%.6f").to_string())
432 }
433}
434impl ToFilterValue for chrono::NaiveDate {
435 fn to_filter_value(&self) -> FilterValue {
436 FilterValue::String(self.format("%Y-%m-%d").to_string())
437 }
438}
439impl ToFilterValue for chrono::NaiveTime {
440 fn to_filter_value(&self) -> FilterValue {
441 FilterValue::String(self.format("%H:%M:%S%.6f").to_string())
442 }
443}
444impl ToFilterValue for serde_json::Value {
445 fn to_filter_value(&self) -> FilterValue {
446 FilterValue::Json(self.clone())
447 }
448}
449impl ToFilterValue for Vec<u8> {
450 fn to_filter_value(&self) -> FilterValue {
451 FilterValue::List(self.iter().map(|b| FilterValue::Int(*b as i64)).collect())
455 }
456}
457impl ToFilterValue for Vec<f32> {
458 fn to_filter_value(&self) -> FilterValue {
459 FilterValue::List(self.iter().map(|f| FilterValue::Float(*f as f64)).collect())
463 }
464}
465impl ToFilterValue for Vec<String> {
466 fn to_filter_value(&self) -> FilterValue {
467 FilterValue::List(self.iter().cloned().map(FilterValue::String).collect())
471 }
472}
473impl<T: ToFilterValue> ToFilterValue for Option<T> {
474 fn to_filter_value(&self) -> FilterValue {
475 self.as_ref()
476 .map(T::to_filter_value)
477 .unwrap_or(FilterValue::Null)
478 }
479}
480
481#[derive(Debug, Clone, PartialEq)]
483pub enum ScalarFilter<T> {
484 Equals(T),
486 Not(Box<T>),
488 In(Vec<T>),
490 NotIn(Vec<T>),
492 Lt(T),
494 Lte(T),
496 Gt(T),
498 Gte(T),
500 Contains(T),
502 StartsWith(T),
504 EndsWith(T),
506 IsNull,
508 IsNotNull,
510}
511
512impl<T: Into<FilterValue>> ScalarFilter<T> {
513 pub fn into_filter(self, column: impl Into<FieldName>) -> Filter {
518 let column = column.into();
519 match self {
520 Self::Equals(v) => Filter::Equals(column, v.into()),
521 Self::Not(v) => Filter::NotEquals(column, (*v).into()),
522 Self::In(values) => Filter::In(column, values.into_iter().map(Into::into).collect()),
523 Self::NotIn(values) => {
524 Filter::NotIn(column, values.into_iter().map(Into::into).collect())
525 }
526 Self::Lt(v) => Filter::Lt(column, v.into()),
527 Self::Lte(v) => Filter::Lte(column, v.into()),
528 Self::Gt(v) => Filter::Gt(column, v.into()),
529 Self::Gte(v) => Filter::Gte(column, v.into()),
530 Self::Contains(v) => Filter::Contains(column, v.into()),
531 Self::StartsWith(v) => Filter::StartsWith(column, v.into()),
532 Self::EndsWith(v) => Filter::EndsWith(column, v.into()),
533 Self::IsNull => Filter::IsNull(column),
534 Self::IsNotNull => Filter::IsNotNull(column),
535 }
536 }
537}
538
539#[derive(Debug, Clone, PartialEq)]
560#[repr(C)] #[derive(Default)]
562#[non_exhaustive]
563pub enum Filter {
564 #[default]
566 None,
567
568 Equals(FieldName, FilterValue),
570 NotEquals(FieldName, FilterValue),
572
573 Lt(FieldName, FilterValue),
575 Lte(FieldName, FilterValue),
577 Gt(FieldName, FilterValue),
579 Gte(FieldName, FilterValue),
581
582 In(FieldName, ValueList),
584 NotIn(FieldName, ValueList),
586
587 Contains(FieldName, FilterValue),
589 StartsWith(FieldName, FilterValue),
591 EndsWith(FieldName, FilterValue),
593
594 IsNull(FieldName),
596 IsNotNull(FieldName),
598
599 And(Box<[Filter]>),
604 Or(Box<[Filter]>),
609 Not(Box<Filter>),
611
612 ScalarSubquery {
622 sql: Cow<'static, str>,
627 params: Vec<FilterValue>,
629 },
630}
631
632fn escape_like_value(s: &str) -> String {
640 s.replace('\\', "\\\\")
641 .replace('%', "\\%")
642 .replace('_', "\\_")
643}
644
645impl Filter {
646 #[inline(always)]
648 pub fn none() -> Self {
649 Self::None
650 }
651
652 #[inline(always)]
654 pub fn is_none(&self) -> bool {
655 matches!(self, Self::None)
656 }
657
658 #[inline]
664 pub fn and(filters: impl IntoIterator<Item = Filter>) -> Self {
665 let filters: Vec<_> = filters.into_iter().filter(|f| !f.is_none()).collect();
666 let count = filters.len();
667 let result = match count {
668 0 => Self::None,
669 1 => filters.into_iter().next().unwrap(),
670 _ => Self::And(filters.into_boxed_slice()),
671 };
672 debug!(count, "Filter::and() created");
673 result
674 }
675
676 #[inline(always)]
680 pub fn and2(a: Filter, b: Filter) -> Self {
681 match (a.is_none(), b.is_none()) {
682 (true, true) => Self::None,
683 (true, false) => b,
684 (false, true) => a,
685 (false, false) => Self::And(Box::new([a, b])),
686 }
687 }
688
689 #[inline]
695 pub fn or(filters: impl IntoIterator<Item = Filter>) -> Self {
696 let filters: Vec<_> = filters.into_iter().filter(|f| !f.is_none()).collect();
697 let count = filters.len();
698 let result = match count {
699 0 => Self::None,
700 1 => filters.into_iter().next().unwrap(),
701 _ => Self::Or(filters.into_boxed_slice()),
702 };
703 debug!(count, "Filter::or() created");
704 result
705 }
706
707 #[inline(always)]
711 pub fn or2(a: Filter, b: Filter) -> Self {
712 match (a.is_none(), b.is_none()) {
713 (true, true) => Self::None,
714 (true, false) => b,
715 (false, true) => a,
716 (false, false) => Self::Or(Box::new([a, b])),
717 }
718 }
719
720 #[inline(always)]
741 pub fn and_n<const N: usize>(filters: [Filter; N]) -> Self {
742 Self::And(Box::new(filters))
744 }
745
746 #[inline(always)]
751 pub fn or_n<const N: usize>(filters: [Filter; N]) -> Self {
752 Self::Or(Box::new(filters))
753 }
754
755 #[inline(always)]
757 pub fn and3(a: Filter, b: Filter, c: Filter) -> Self {
758 Self::And(Box::new([a, b, c]))
759 }
760
761 #[inline(always)]
763 pub fn and4(a: Filter, b: Filter, c: Filter, d: Filter) -> Self {
764 Self::And(Box::new([a, b, c, d]))
765 }
766
767 #[inline(always)]
769 pub fn and5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Self {
770 Self::And(Box::new([a, b, c, d, e]))
771 }
772
773 #[inline(always)]
775 pub fn or3(a: Filter, b: Filter, c: Filter) -> Self {
776 Self::Or(Box::new([a, b, c]))
777 }
778
779 #[inline(always)]
781 pub fn or4(a: Filter, b: Filter, c: Filter, d: Filter) -> Self {
782 Self::Or(Box::new([a, b, c, d]))
783 }
784
785 #[inline(always)]
787 pub fn or5(a: Filter, b: Filter, c: Filter, d: Filter, e: Filter) -> Self {
788 Self::Or(Box::new([a, b, c, d, e]))
789 }
790
791 #[inline]
800 pub fn in_i64(field: impl Into<FieldName>, values: impl IntoIterator<Item = i64>) -> Self {
801 let list: ValueList = values.into_iter().map(FilterValue::Int).collect();
802 Self::In(field.into(), list)
803 }
804
805 #[inline]
807 pub fn in_i32(field: impl Into<FieldName>, values: impl IntoIterator<Item = i32>) -> Self {
808 let list: ValueList = values
809 .into_iter()
810 .map(|v| FilterValue::Int(v as i64))
811 .collect();
812 Self::In(field.into(), list)
813 }
814
815 #[inline]
817 pub fn in_strings(
818 field: impl Into<FieldName>,
819 values: impl IntoIterator<Item = String>,
820 ) -> Self {
821 let list: ValueList = values.into_iter().map(FilterValue::String).collect();
822 Self::In(field.into(), list)
823 }
824
825 #[inline]
829 pub fn in_values(field: impl Into<FieldName>, values: ValueList) -> Self {
830 Self::In(field.into(), values)
831 }
832
833 #[inline]
837 pub fn in_range(field: impl Into<FieldName>, range: std::ops::Range<i64>) -> Self {
838 let list: ValueList = range.map(FilterValue::Int).collect();
839 Self::In(field.into(), list)
840 }
841
842 #[inline(always)]
847 pub fn in_i64_slice(field: impl Into<FieldName>, values: &[i64]) -> Self {
848 let mut list = Vec::with_capacity(values.len());
849 for &v in values {
850 list.push(FilterValue::Int(v));
851 }
852 Self::In(field.into(), list)
853 }
854
855 #[inline(always)]
857 pub fn in_i32_slice(field: impl Into<FieldName>, values: &[i32]) -> Self {
858 let mut list = Vec::with_capacity(values.len());
859 for &v in values {
860 list.push(FilterValue::Int(v as i64));
861 }
862 Self::In(field.into(), list)
863 }
864
865 #[inline(always)]
867 pub fn in_str_slice(field: impl Into<FieldName>, values: &[&str]) -> Self {
868 let mut list = Vec::with_capacity(values.len());
869 for &v in values {
870 list.push(FilterValue::String(v.to_string()));
871 }
872 Self::In(field.into(), list)
873 }
874
875 #[inline]
877 #[allow(clippy::should_implement_trait)]
878 pub fn not(filter: Filter) -> Self {
879 if filter.is_none() {
880 return Self::None;
881 }
882 Self::Not(Box::new(filter))
883 }
884
885 #[inline]
899 pub fn in_slice<T: Into<FilterValue> + Clone>(
900 field: impl Into<FieldName>,
901 values: &[T],
902 ) -> Self {
903 let list: ValueList = values.iter().map(|v| v.clone().into()).collect();
904 Self::In(field.into(), list)
905 }
906
907 #[inline]
918 pub fn not_in_slice<T: Into<FilterValue> + Clone>(
919 field: impl Into<FieldName>,
920 values: &[T],
921 ) -> Self {
922 let list: ValueList = values.iter().map(|v| v.clone().into()).collect();
923 Self::NotIn(field.into(), list)
924 }
925
926 #[inline]
938 pub fn in_array<T: Into<FilterValue>, const N: usize>(
939 field: impl Into<FieldName>,
940 values: [T; N],
941 ) -> Self {
942 let list: ValueList = values.into_iter().map(Into::into).collect();
943 Self::In(field.into(), list)
944 }
945
946 #[inline]
948 pub fn not_in_array<T: Into<FilterValue>, const N: usize>(
949 field: impl Into<FieldName>,
950 values: [T; N],
951 ) -> Self {
952 let list: ValueList = values.into_iter().map(Into::into).collect();
953 Self::NotIn(field.into(), list)
954 }
955
956 pub fn and_then(self, other: Filter) -> Self {
958 if self.is_none() {
959 return other;
960 }
961 if other.is_none() {
962 return self;
963 }
964 match self {
965 Self::And(filters) => {
966 let mut vec: Vec<_> = filters.into_vec();
968 vec.push(other);
969 Self::And(vec.into_boxed_slice())
970 }
971 _ => Self::And(Box::new([self, other])),
972 }
973 }
974
975 pub fn or_else(self, other: Filter) -> Self {
977 if self.is_none() {
978 return other;
979 }
980 if other.is_none() {
981 return self;
982 }
983 match self {
984 Self::Or(filters) => {
985 let mut vec: Vec<_> = filters.into_vec();
987 vec.push(other);
988 Self::Or(vec.into_boxed_slice())
989 }
990 _ => Self::Or(Box::new([self, other])),
991 }
992 }
993
994 pub fn to_sql(
997 &self,
998 param_offset: usize,
999 dialect: &dyn crate::dialect::SqlDialect,
1000 ) -> (String, Vec<FilterValue>) {
1001 let mut params = Vec::new();
1002 let sql = self.to_sql_with_params(param_offset, &mut params, dialect);
1003 (sql, params)
1004 }
1005
1006 fn to_sql_with_params(
1027 &self,
1028 param_idx: usize,
1029 params: &mut Vec<FilterValue>,
1030 dialect: &dyn crate::dialect::SqlDialect,
1031 ) -> String {
1032 match self {
1033 Self::None => "TRUE".to_string(),
1034
1035 Self::Equals(col, val) => {
1036 let c = dialect.quote_ident(col);
1037 if val.is_null() {
1038 format!("{} IS NULL", c)
1039 } else {
1040 params.push(val.clone());
1041 format!("{} = {}", c, dialect.placeholder(param_idx + 1))
1042 }
1043 }
1044 Self::NotEquals(col, val) => {
1045 let c = dialect.quote_ident(col);
1046 if val.is_null() {
1047 format!("{} IS NOT NULL", c)
1048 } else {
1049 params.push(val.clone());
1050 format!("{} != {}", c, dialect.placeholder(param_idx + 1))
1051 }
1052 }
1053
1054 Self::Lt(col, val) => {
1055 let c = dialect.quote_ident(col);
1056 params.push(val.clone());
1057 format!("{} < {}", c, dialect.placeholder(param_idx + 1))
1058 }
1059 Self::Lte(col, val) => {
1060 let c = dialect.quote_ident(col);
1061 params.push(val.clone());
1062 format!("{} <= {}", c, dialect.placeholder(param_idx + 1))
1063 }
1064 Self::Gt(col, val) => {
1065 let c = dialect.quote_ident(col);
1066 params.push(val.clone());
1067 format!("{} > {}", c, dialect.placeholder(param_idx + 1))
1068 }
1069 Self::Gte(col, val) => {
1070 let c = dialect.quote_ident(col);
1071 params.push(val.clone());
1072 format!("{} >= {}", c, dialect.placeholder(param_idx + 1))
1073 }
1074
1075 Self::In(col, values) => {
1076 if values.is_empty() {
1077 return "FALSE".to_string();
1078 }
1079 let c = dialect.quote_ident(col);
1080 let placeholders: Vec<_> = values
1081 .iter()
1082 .enumerate()
1083 .map(|(i, v)| {
1084 params.push(v.clone());
1085 dialect.placeholder(param_idx + i + 1)
1086 })
1087 .collect();
1088 format!("{} IN ({})", c, placeholders.join(", "))
1089 }
1090 Self::NotIn(col, values) => {
1091 if values.is_empty() {
1092 return "TRUE".to_string();
1093 }
1094 let c = dialect.quote_ident(col);
1095 let placeholders: Vec<_> = values
1096 .iter()
1097 .enumerate()
1098 .map(|(i, v)| {
1099 params.push(v.clone());
1100 dialect.placeholder(param_idx + i + 1)
1101 })
1102 .collect();
1103 format!("{} NOT IN ({})", c, placeholders.join(", "))
1104 }
1105
1106 Self::Contains(col, val) => {
1107 let c = dialect.quote_ident(col);
1108 if let FilterValue::String(s) = val {
1109 params.push(FilterValue::String(format!("%{}%", escape_like_value(s))));
1110 } else {
1111 params.push(val.clone());
1112 }
1113 format!(
1114 "{} LIKE {}{}",
1115 c,
1116 dialect.placeholder(param_idx + 1),
1117 dialect.like_escape_clause()
1118 )
1119 }
1120 Self::StartsWith(col, val) => {
1121 let c = dialect.quote_ident(col);
1122 if let FilterValue::String(s) = val {
1123 params.push(FilterValue::String(format!("{}%", escape_like_value(s))));
1124 } else {
1125 params.push(val.clone());
1126 }
1127 format!(
1128 "{} LIKE {}{}",
1129 c,
1130 dialect.placeholder(param_idx + 1),
1131 dialect.like_escape_clause()
1132 )
1133 }
1134 Self::EndsWith(col, val) => {
1135 let c = dialect.quote_ident(col);
1136 if let FilterValue::String(s) = val {
1137 params.push(FilterValue::String(format!("%{}", escape_like_value(s))));
1138 } else {
1139 params.push(val.clone());
1140 }
1141 format!(
1142 "{} LIKE {}{}",
1143 c,
1144 dialect.placeholder(param_idx + 1),
1145 dialect.like_escape_clause()
1146 )
1147 }
1148
1149 Self::IsNull(col) => {
1150 let c = dialect.quote_ident(col);
1151 format!("{} IS NULL", c)
1152 }
1153 Self::IsNotNull(col) => {
1154 let c = dialect.quote_ident(col);
1155 format!("{} IS NOT NULL", c)
1156 }
1157
1158 Self::And(filters) => {
1159 if filters.is_empty() {
1160 return "TRUE".to_string();
1161 }
1162 let base = param_idx - params.len();
1167 let parts: Vec<_> = filters
1168 .iter()
1169 .map(|f| f.to_sql_with_params(base + params.len(), params, dialect))
1170 .collect();
1171 format!("({})", parts.join(" AND "))
1172 }
1173 Self::Or(filters) => {
1174 if filters.is_empty() {
1175 return "FALSE".to_string();
1176 }
1177 let base = param_idx - params.len();
1178 let parts: Vec<_> = filters
1179 .iter()
1180 .map(|f| f.to_sql_with_params(base + params.len(), params, dialect))
1181 .collect();
1182 format!("({})", parts.join(" OR "))
1183 }
1184 Self::Not(filter) => {
1185 let inner = filter.to_sql_with_params(param_idx, params, dialect);
1186 format!("NOT ({})", inner)
1187 }
1188
1189 Self::ScalarSubquery {
1190 sql,
1191 params: inner_params,
1192 } => {
1193 let base = param_idx;
1198 for v in inner_params.iter() {
1202 params.push(v.clone());
1203 }
1204 let mut out = String::with_capacity(sql.len() + inner_params.len() * 4);
1205 let mut chars = sql.chars().peekable();
1206 while let Some(ch) = chars.next() {
1207 if ch == '{' {
1208 let mut digits = String::new();
1210 while let Some(&c) = chars.peek() {
1211 if c == '}' {
1212 chars.next();
1213 break;
1214 }
1215 digits.push(c);
1216 chars.next();
1217 }
1218 let n: usize = digits.parse().unwrap_or_else(|_| {
1219 panic!(
1220 "Filter::ScalarSubquery: invalid placeholder index `{{{}}}`",
1221 digits
1222 )
1223 });
1224 if n >= inner_params.len() {
1225 panic!(
1226 "Filter::ScalarSubquery: placeholder {{{}}} out of range (have {} params)",
1227 n,
1228 inner_params.len()
1229 );
1230 }
1231 out.push_str(&dialect.placeholder(base + n + 1));
1232 } else {
1233 out.push(ch);
1234 }
1235 }
1236 format!("({})", out)
1237 }
1238 }
1239 }
1240
1241 #[inline]
1259 pub fn and_builder(capacity: usize) -> AndFilterBuilder {
1260 AndFilterBuilder::with_capacity(capacity)
1261 }
1262
1263 #[inline]
1280 pub fn or_builder(capacity: usize) -> OrFilterBuilder {
1281 OrFilterBuilder::with_capacity(capacity)
1282 }
1283
1284 #[inline]
1300 pub fn builder() -> FluentFilterBuilder {
1301 FluentFilterBuilder::new()
1302 }
1303}
1304
1305#[derive(Debug, Clone)]
1309pub struct AndFilterBuilder {
1310 filters: Vec<Filter>,
1311}
1312
1313impl AndFilterBuilder {
1314 #[inline]
1316 pub fn new() -> Self {
1317 Self {
1318 filters: Vec::new(),
1319 }
1320 }
1321
1322 #[inline]
1324 pub fn with_capacity(capacity: usize) -> Self {
1325 Self {
1326 filters: Vec::with_capacity(capacity),
1327 }
1328 }
1329
1330 #[inline]
1332 pub fn push(mut self, filter: Filter) -> Self {
1333 if !filter.is_none() {
1334 self.filters.push(filter);
1335 }
1336 self
1337 }
1338
1339 #[inline]
1341 pub fn extend(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
1342 self.filters
1343 .extend(filters.into_iter().filter(|f| !f.is_none()));
1344 self
1345 }
1346
1347 #[inline]
1349 pub fn push_if(self, condition: bool, filter: Filter) -> Self {
1350 if condition { self.push(filter) } else { self }
1351 }
1352
1353 #[inline]
1355 pub fn push_if_some<F>(self, opt: Option<F>) -> Self
1356 where
1357 F: Into<Filter>,
1358 {
1359 match opt {
1360 Some(f) => self.push(f.into()),
1361 None => self,
1362 }
1363 }
1364
1365 #[inline]
1367 pub fn build(self) -> Filter {
1368 match self.filters.len() {
1369 0 => Filter::None,
1370 1 => self.filters.into_iter().next().unwrap(),
1371 _ => Filter::And(self.filters.into_boxed_slice()),
1372 }
1373 }
1374
1375 #[inline]
1377 pub fn len(&self) -> usize {
1378 self.filters.len()
1379 }
1380
1381 #[inline]
1383 pub fn is_empty(&self) -> bool {
1384 self.filters.is_empty()
1385 }
1386}
1387
1388impl Default for AndFilterBuilder {
1389 fn default() -> Self {
1390 Self::new()
1391 }
1392}
1393
1394#[derive(Debug, Clone)]
1398pub struct OrFilterBuilder {
1399 filters: Vec<Filter>,
1400}
1401
1402impl OrFilterBuilder {
1403 #[inline]
1405 pub fn new() -> Self {
1406 Self {
1407 filters: Vec::new(),
1408 }
1409 }
1410
1411 #[inline]
1413 pub fn with_capacity(capacity: usize) -> Self {
1414 Self {
1415 filters: Vec::with_capacity(capacity),
1416 }
1417 }
1418
1419 #[inline]
1421 pub fn push(mut self, filter: Filter) -> Self {
1422 if !filter.is_none() {
1423 self.filters.push(filter);
1424 }
1425 self
1426 }
1427
1428 #[inline]
1430 pub fn extend(mut self, filters: impl IntoIterator<Item = Filter>) -> Self {
1431 self.filters
1432 .extend(filters.into_iter().filter(|f| !f.is_none()));
1433 self
1434 }
1435
1436 #[inline]
1438 pub fn push_if(self, condition: bool, filter: Filter) -> Self {
1439 if condition { self.push(filter) } else { self }
1440 }
1441
1442 #[inline]
1444 pub fn push_if_some<F>(self, opt: Option<F>) -> Self
1445 where
1446 F: Into<Filter>,
1447 {
1448 match opt {
1449 Some(f) => self.push(f.into()),
1450 None => self,
1451 }
1452 }
1453
1454 #[inline]
1456 pub fn build(self) -> Filter {
1457 match self.filters.len() {
1458 0 => Filter::None,
1459 1 => self.filters.into_iter().next().unwrap(),
1460 _ => Filter::Or(self.filters.into_boxed_slice()),
1461 }
1462 }
1463
1464 #[inline]
1466 pub fn len(&self) -> usize {
1467 self.filters.len()
1468 }
1469
1470 #[inline]
1472 pub fn is_empty(&self) -> bool {
1473 self.filters.is_empty()
1474 }
1475}
1476
1477impl Default for OrFilterBuilder {
1478 fn default() -> Self {
1479 Self::new()
1480 }
1481}
1482
1483#[derive(Debug, Clone)]
1508pub struct FluentFilterBuilder {
1509 filters: Vec<Filter>,
1510}
1511
1512impl FluentFilterBuilder {
1513 #[inline]
1515 pub fn new() -> Self {
1516 Self {
1517 filters: Vec::new(),
1518 }
1519 }
1520
1521 #[inline]
1523 pub fn with_capacity(mut self, capacity: usize) -> Self {
1524 self.filters.reserve(capacity);
1525 self
1526 }
1527
1528 #[inline]
1530 pub fn eq<F, V>(mut self, field: F, value: V) -> Self
1531 where
1532 F: Into<FieldName>,
1533 V: Into<FilterValue>,
1534 {
1535 self.filters
1536 .push(Filter::Equals(field.into(), value.into()));
1537 self
1538 }
1539
1540 #[inline]
1542 pub fn ne<F, V>(mut self, field: F, value: V) -> Self
1543 where
1544 F: Into<FieldName>,
1545 V: Into<FilterValue>,
1546 {
1547 self.filters
1548 .push(Filter::NotEquals(field.into(), value.into()));
1549 self
1550 }
1551
1552 #[inline]
1554 pub fn lt<F, V>(mut self, field: F, value: V) -> Self
1555 where
1556 F: Into<FieldName>,
1557 V: Into<FilterValue>,
1558 {
1559 self.filters.push(Filter::Lt(field.into(), value.into()));
1560 self
1561 }
1562
1563 #[inline]
1565 pub fn lte<F, V>(mut self, field: F, value: V) -> Self
1566 where
1567 F: Into<FieldName>,
1568 V: Into<FilterValue>,
1569 {
1570 self.filters.push(Filter::Lte(field.into(), value.into()));
1571 self
1572 }
1573
1574 #[inline]
1576 pub fn gt<F, V>(mut self, field: F, value: V) -> Self
1577 where
1578 F: Into<FieldName>,
1579 V: Into<FilterValue>,
1580 {
1581 self.filters.push(Filter::Gt(field.into(), value.into()));
1582 self
1583 }
1584
1585 #[inline]
1587 pub fn gte<F, V>(mut self, field: F, value: V) -> Self
1588 where
1589 F: Into<FieldName>,
1590 V: Into<FilterValue>,
1591 {
1592 self.filters.push(Filter::Gte(field.into(), value.into()));
1593 self
1594 }
1595
1596 #[inline]
1598 pub fn is_in<F, I, V>(mut self, field: F, values: I) -> Self
1599 where
1600 F: Into<FieldName>,
1601 I: IntoIterator<Item = V>,
1602 V: Into<FilterValue>,
1603 {
1604 self.filters.push(Filter::In(
1605 field.into(),
1606 values.into_iter().map(Into::into).collect(),
1607 ));
1608 self
1609 }
1610
1611 #[inline]
1613 pub fn not_in<F, I, V>(mut self, field: F, values: I) -> Self
1614 where
1615 F: Into<FieldName>,
1616 I: IntoIterator<Item = V>,
1617 V: Into<FilterValue>,
1618 {
1619 self.filters.push(Filter::NotIn(
1620 field.into(),
1621 values.into_iter().map(Into::into).collect(),
1622 ));
1623 self
1624 }
1625
1626 #[inline]
1628 pub fn contains<F, V>(mut self, field: F, value: V) -> Self
1629 where
1630 F: Into<FieldName>,
1631 V: Into<FilterValue>,
1632 {
1633 self.filters
1634 .push(Filter::Contains(field.into(), value.into()));
1635 self
1636 }
1637
1638 #[inline]
1640 pub fn starts_with<F, V>(mut self, field: F, value: V) -> Self
1641 where
1642 F: Into<FieldName>,
1643 V: Into<FilterValue>,
1644 {
1645 self.filters
1646 .push(Filter::StartsWith(field.into(), value.into()));
1647 self
1648 }
1649
1650 #[inline]
1652 pub fn ends_with<F, V>(mut self, field: F, value: V) -> Self
1653 where
1654 F: Into<FieldName>,
1655 V: Into<FilterValue>,
1656 {
1657 self.filters
1658 .push(Filter::EndsWith(field.into(), value.into()));
1659 self
1660 }
1661
1662 #[inline]
1664 pub fn is_null<F>(mut self, field: F) -> Self
1665 where
1666 F: Into<FieldName>,
1667 {
1668 self.filters.push(Filter::IsNull(field.into()));
1669 self
1670 }
1671
1672 #[inline]
1674 pub fn is_not_null<F>(mut self, field: F) -> Self
1675 where
1676 F: Into<FieldName>,
1677 {
1678 self.filters.push(Filter::IsNotNull(field.into()));
1679 self
1680 }
1681
1682 #[inline]
1684 pub fn filter(mut self, filter: Filter) -> Self {
1685 if !filter.is_none() {
1686 self.filters.push(filter);
1687 }
1688 self
1689 }
1690
1691 #[inline]
1693 pub fn filter_if(self, condition: bool, filter: Filter) -> Self {
1694 if condition { self.filter(filter) } else { self }
1695 }
1696
1697 #[inline]
1699 pub fn filter_if_some<F>(self, opt: Option<F>) -> Self
1700 where
1701 F: Into<Filter>,
1702 {
1703 match opt {
1704 Some(f) => self.filter(f.into()),
1705 None => self,
1706 }
1707 }
1708
1709 #[inline]
1711 pub fn build_and(self) -> Filter {
1712 let filters: Vec<_> = self.filters.into_iter().filter(|f| !f.is_none()).collect();
1713 match filters.len() {
1714 0 => Filter::None,
1715 1 => filters.into_iter().next().unwrap(),
1716 _ => Filter::And(filters.into_boxed_slice()),
1717 }
1718 }
1719
1720 #[inline]
1722 pub fn build_or(self) -> Filter {
1723 let filters: Vec<_> = self.filters.into_iter().filter(|f| !f.is_none()).collect();
1724 match filters.len() {
1725 0 => Filter::None,
1726 1 => filters.into_iter().next().unwrap(),
1727 _ => Filter::Or(filters.into_boxed_slice()),
1728 }
1729 }
1730
1731 #[inline]
1733 pub fn len(&self) -> usize {
1734 self.filters.len()
1735 }
1736
1737 #[inline]
1739 pub fn is_empty(&self) -> bool {
1740 self.filters.is_empty()
1741 }
1742}
1743
1744impl Default for FluentFilterBuilder {
1745 fn default() -> Self {
1746 Self::new()
1747 }
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752 use super::*;
1753
1754 #[test]
1755 fn test_filter_value_from() {
1756 assert_eq!(FilterValue::from(42i32), FilterValue::Int(42));
1757 assert_eq!(
1758 FilterValue::from("hello"),
1759 FilterValue::String("hello".to_string())
1760 );
1761 assert_eq!(FilterValue::from(true), FilterValue::Bool(true));
1762 }
1763
1764 #[test]
1765 fn test_scalar_filter_equals() {
1766 let filter = ScalarFilter::Equals("test@example.com".to_string()).into_filter("email");
1767
1768 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1769 assert_eq!(sql, r#""email" = $1"#);
1770 assert_eq!(params.len(), 1);
1771 }
1772
1773 #[test]
1774 fn test_filter_and() {
1775 let f1 = Filter::Equals("name".into(), "Alice".into());
1776 let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
1777 let combined = Filter::and([f1, f2]);
1778
1779 let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
1780 assert!(sql.contains("AND"));
1781 assert_eq!(params.len(), 2);
1782 }
1783
1784 #[test]
1785 fn test_filter_or() {
1786 let f1 = Filter::Equals("status".into(), "active".into());
1787 let f2 = Filter::Equals("status".into(), "pending".into());
1788 let combined = Filter::or([f1, f2]);
1789
1790 let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
1791 assert!(sql.contains("OR"));
1792 }
1793
1794 #[test]
1795 fn test_filter_not() {
1796 let filter = Filter::not(Filter::Equals("deleted".into(), FilterValue::Bool(true)));
1797
1798 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
1799 assert!(sql.contains("NOT"));
1800 }
1801
1802 #[test]
1803 fn test_filter_is_null() {
1804 let filter = Filter::IsNull("deleted_at".into());
1805 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1806 assert_eq!(sql, r#""deleted_at" IS NULL"#);
1807 assert!(params.is_empty());
1808 }
1809
1810 #[test]
1811 fn test_filter_in() {
1812 let filter = Filter::In("status".into(), vec!["active".into(), "pending".into()]);
1813 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1814 assert!(sql.contains("IN"));
1815 assert_eq!(params.len(), 2);
1816 }
1817
1818 #[test]
1819 fn test_filter_contains() {
1820 let filter = Filter::Contains("email".into(), "example".into());
1821 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1822 assert!(sql.contains("LIKE"));
1823 assert!(sql.contains(r"ESCAPE '\'"));
1824 assert_eq!(params.len(), 1);
1825 if let FilterValue::String(s) = ¶ms[0] {
1826 assert!(s.contains("%example%"));
1827 }
1828 }
1829
1830 #[test]
1831 fn test_filter_like_escapes_wildcards() {
1832 let filter = Filter::Contains("name".into(), "100%".into());
1834 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1835 assert!(sql.contains(r"ESCAPE '\'"));
1836 assert_eq!(params.len(), 1);
1837 assert_eq!(params[0], FilterValue::String(r"%100\%%".to_string()));
1838
1839 let filter = Filter::StartsWith("name".into(), "a_b".into());
1841 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1842 assert!(sql.contains(r"ESCAPE '\'"));
1843 assert_eq!(params.len(), 1);
1844 assert_eq!(params[0], FilterValue::String(r"a\_b%".to_string()));
1845
1846 let filter = Filter::EndsWith("path".into(), r"a\b".into());
1848 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1849 assert!(sql.contains(r"ESCAPE '\'"));
1850 assert_eq!(params.len(), 1);
1851 assert_eq!(params[0], FilterValue::String(r"%a\\b".to_string()));
1852 }
1853
1854 #[test]
1855 fn test_filter_like_escape_clause_mysql() {
1856 use crate::dialect::Mysql;
1860
1861 let filter = Filter::Contains("name".into(), "100%".into());
1862 let (sql, params) = filter.to_sql(0, &Mysql);
1863 assert!(sql.contains(r"ESCAPE '\\'"), "MySQL escape clause: {sql}");
1864 assert!(!sql.contains(r"ESCAPE '\'"), "MySQL escape clause: {sql}");
1865 assert_eq!(params.len(), 1);
1866 assert_eq!(params[0], FilterValue::String(r"%100\%%".to_string()));
1867
1868 let filter = Filter::StartsWith("name".into(), "a_b".into());
1870 let (sql, _) = filter.to_sql(0, &Mysql);
1871 assert!(sql.contains(r"ESCAPE '\\'"), "MySQL escape clause: {sql}");
1872
1873 let filter = Filter::EndsWith("name".into(), "a_b".into());
1874 let (sql, _) = filter.to_sql(0, &Mysql);
1875 assert!(sql.contains(r"ESCAPE '\\'"), "MySQL escape clause: {sql}");
1876 }
1877
1878 #[test]
1881 fn test_filter_value_is_null() {
1882 assert!(FilterValue::Null.is_null());
1883 assert!(!FilterValue::Bool(false).is_null());
1884 assert!(!FilterValue::Int(0).is_null());
1885 assert!(!FilterValue::Float(0.0).is_null());
1886 assert!(!FilterValue::String("".to_string()).is_null());
1887 }
1888
1889 #[test]
1890 fn test_filter_value_from_i64() {
1891 assert_eq!(FilterValue::from(42i64), FilterValue::Int(42));
1892 assert_eq!(FilterValue::from(-100i64), FilterValue::Int(-100));
1893 }
1894
1895 #[test]
1896 #[allow(clippy::approx_constant)]
1897 fn test_filter_value_from_f64() {
1898 assert_eq!(FilterValue::from(3.14f64), FilterValue::Float(3.14));
1899 }
1900
1901 #[test]
1902 fn test_filter_value_from_string() {
1903 assert_eq!(
1904 FilterValue::from("hello".to_string()),
1905 FilterValue::String("hello".to_string())
1906 );
1907 }
1908
1909 #[test]
1910 fn vec_string_to_filter_value_is_a_list_of_strings() {
1911 let scopes = vec!["read".to_string(), "write".to_string()];
1912 assert_eq!(
1913 scopes.to_filter_value(),
1914 FilterValue::List(vec![
1915 FilterValue::String("read".into()),
1916 FilterValue::String("write".into()),
1917 ])
1918 );
1919 assert_eq!(
1920 Vec::<String>::new().to_filter_value(),
1921 FilterValue::List(vec![])
1922 );
1923 assert_eq!(
1924 Option::<Vec<String>>::None.to_filter_value(),
1925 FilterValue::Null
1926 );
1927 }
1928
1929 #[test]
1930 fn test_filter_value_from_vec() {
1931 let values: Vec<i32> = vec![1, 2, 3];
1932 let filter_val: FilterValue = values.into();
1933 if let FilterValue::List(list) = filter_val {
1934 assert_eq!(list.len(), 3);
1935 assert_eq!(list[0], FilterValue::Int(1));
1936 assert_eq!(list[1], FilterValue::Int(2));
1937 assert_eq!(list[2], FilterValue::Int(3));
1938 } else {
1939 panic!("Expected List");
1940 }
1941 }
1942
1943 #[test]
1944 fn test_filter_value_from_option_some() {
1945 let val: FilterValue = Some(42i32).into();
1946 assert_eq!(val, FilterValue::Int(42));
1947 }
1948
1949 #[test]
1950 fn test_filter_value_from_option_none() {
1951 let val: FilterValue = Option::<i32>::None.into();
1952 assert_eq!(val, FilterValue::Null);
1953 }
1954
1955 #[test]
1958 fn test_scalar_filter_not() {
1959 let filter = ScalarFilter::Not(Box::new("test".to_string())).into_filter("name");
1960 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1961 assert_eq!(sql, r#""name" != $1"#);
1962 assert_eq!(params.len(), 1);
1963 }
1964
1965 #[test]
1966 fn test_scalar_filter_in() {
1967 let filter = ScalarFilter::In(vec!["a".to_string(), "b".to_string()]).into_filter("status");
1968 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1969 assert!(sql.contains("IN"));
1970 assert_eq!(params.len(), 2);
1971 }
1972
1973 #[test]
1974 fn test_scalar_filter_not_in() {
1975 let filter = ScalarFilter::NotIn(vec!["x".to_string()]).into_filter("status");
1976 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1977 assert!(sql.contains("NOT IN"));
1978 assert_eq!(params.len(), 1);
1979 }
1980
1981 #[test]
1982 fn test_scalar_filter_lt() {
1983 let filter = ScalarFilter::Lt(100i32).into_filter("price");
1984 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1985 assert_eq!(sql, r#""price" < $1"#);
1986 assert_eq!(params.len(), 1);
1987 }
1988
1989 #[test]
1990 fn test_scalar_filter_lte() {
1991 let filter = ScalarFilter::Lte(100i32).into_filter("price");
1992 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
1993 assert_eq!(sql, r#""price" <= $1"#);
1994 assert_eq!(params.len(), 1);
1995 }
1996
1997 #[test]
1998 fn test_scalar_filter_gt() {
1999 let filter = ScalarFilter::Gt(0i32).into_filter("quantity");
2000 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2001 assert_eq!(sql, r#""quantity" > $1"#);
2002 assert_eq!(params.len(), 1);
2003 }
2004
2005 #[test]
2006 fn test_scalar_filter_gte() {
2007 let filter = ScalarFilter::Gte(0i32).into_filter("quantity");
2008 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2009 assert_eq!(sql, r#""quantity" >= $1"#);
2010 assert_eq!(params.len(), 1);
2011 }
2012
2013 #[test]
2014 fn test_scalar_filter_starts_with() {
2015 let filter = ScalarFilter::StartsWith("prefix".to_string()).into_filter("name");
2016 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2017 assert!(sql.contains("LIKE"));
2018 assert_eq!(params.len(), 1);
2019 if let FilterValue::String(s) = ¶ms[0] {
2020 assert!(s.starts_with("prefix"));
2021 assert!(s.ends_with("%"));
2022 }
2023 }
2024
2025 #[test]
2026 fn test_scalar_filter_ends_with() {
2027 let filter = ScalarFilter::EndsWith("suffix".to_string()).into_filter("name");
2028 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2029 assert!(sql.contains("LIKE"));
2030 assert_eq!(params.len(), 1);
2031 if let FilterValue::String(s) = ¶ms[0] {
2032 assert!(s.starts_with("%"));
2033 assert!(s.ends_with("suffix"));
2034 }
2035 }
2036
2037 #[test]
2038 fn test_scalar_filter_is_null() {
2039 let filter = ScalarFilter::<String>::IsNull.into_filter("deleted_at");
2040 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2041 assert_eq!(sql, r#""deleted_at" IS NULL"#);
2042 assert!(params.is_empty());
2043 }
2044
2045 #[test]
2046 fn test_scalar_filter_is_not_null() {
2047 let filter = ScalarFilter::<String>::IsNotNull.into_filter("name");
2048 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2049 assert_eq!(sql, r#""name" IS NOT NULL"#);
2050 assert!(params.is_empty());
2051 }
2052
2053 #[test]
2056 fn test_filter_none() {
2057 let filter = Filter::none();
2058 assert!(filter.is_none());
2059 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2060 assert_eq!(sql, "TRUE"); assert!(params.is_empty());
2062 }
2063
2064 #[test]
2065 fn test_filter_not_equals() {
2066 let filter = Filter::NotEquals("status".into(), "deleted".into());
2067 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2068 assert_eq!(sql, r#""status" != $1"#);
2069 assert_eq!(params.len(), 1);
2070 }
2071
2072 #[test]
2073 fn test_filter_lte() {
2074 let filter = Filter::Lte("price".into(), FilterValue::Int(100));
2075 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2076 assert_eq!(sql, r#""price" <= $1"#);
2077 assert_eq!(params.len(), 1);
2078 }
2079
2080 #[test]
2081 fn test_filter_gte() {
2082 let filter = Filter::Gte("quantity".into(), FilterValue::Int(0));
2083 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2084 assert_eq!(sql, r#""quantity" >= $1"#);
2085 assert_eq!(params.len(), 1);
2086 }
2087
2088 #[test]
2089 fn test_filter_not_in() {
2090 let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
2091 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2092 assert!(sql.contains("NOT IN"));
2093 assert_eq!(params.len(), 2);
2094 }
2095
2096 #[test]
2097 fn test_filter_starts_with() {
2098 let filter = Filter::StartsWith("email".into(), "admin".into());
2099 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2100 assert!(sql.contains("LIKE"));
2101 assert_eq!(params.len(), 1);
2102 }
2103
2104 #[test]
2105 fn test_filter_ends_with() {
2106 let filter = Filter::EndsWith("email".into(), "@example.com".into());
2107 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2108 assert!(sql.contains("LIKE"));
2109 assert_eq!(params.len(), 1);
2110 }
2111
2112 #[test]
2113 fn test_filter_is_not_null() {
2114 let filter = Filter::IsNotNull("name".into());
2115 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2116 assert_eq!(sql, r#""name" IS NOT NULL"#);
2117 assert!(params.is_empty());
2118 }
2119
2120 #[test]
2123 fn test_filter_and_empty() {
2124 let filter = Filter::and([]);
2125 assert!(filter.is_none());
2126 }
2127
2128 #[test]
2129 fn test_filter_and_single() {
2130 let f = Filter::Equals("name".into(), "Alice".into());
2131 let combined = Filter::and([f.clone()]);
2132 assert_eq!(combined, f);
2133 }
2134
2135 #[test]
2136 fn test_filter_and_with_none() {
2137 let f1 = Filter::Equals("name".into(), "Alice".into());
2138 let f2 = Filter::None;
2139 let combined = Filter::and([f1.clone(), f2]);
2140 assert_eq!(combined, f1);
2141 }
2142
2143 #[test]
2144 fn test_filter_or_empty() {
2145 let filter = Filter::or([]);
2146 assert!(filter.is_none());
2147 }
2148
2149 #[test]
2150 fn test_filter_or_single() {
2151 let f = Filter::Equals("status".into(), "active".into());
2152 let combined = Filter::or([f.clone()]);
2153 assert_eq!(combined, f);
2154 }
2155
2156 #[test]
2157 fn test_filter_or_with_none() {
2158 let f1 = Filter::Equals("status".into(), "active".into());
2159 let f2 = Filter::None;
2160 let combined = Filter::or([f1.clone(), f2]);
2161 assert_eq!(combined, f1);
2162 }
2163
2164 #[test]
2165 fn test_filter_not_none() {
2166 let filter = Filter::not(Filter::None);
2167 assert!(filter.is_none());
2168 }
2169
2170 #[test]
2171 fn test_filter_and_then() {
2172 let f1 = Filter::Equals("name".into(), "Alice".into());
2173 let f2 = Filter::Gt("age".into(), FilterValue::Int(18));
2174 let combined = f1.and_then(f2);
2175
2176 let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2177 assert!(sql.contains("AND"));
2178 assert_eq!(params.len(), 2);
2179 }
2180
2181 #[test]
2182 fn test_filter_and_then_with_none_first() {
2183 let f1 = Filter::None;
2184 let f2 = Filter::Equals("name".into(), "Bob".into());
2185 let combined = f1.and_then(f2.clone());
2186 assert_eq!(combined, f2);
2187 }
2188
2189 #[test]
2190 fn test_filter_and_then_with_none_second() {
2191 let f1 = Filter::Equals("name".into(), "Alice".into());
2192 let f2 = Filter::None;
2193 let combined = f1.clone().and_then(f2);
2194 assert_eq!(combined, f1);
2195 }
2196
2197 #[test]
2198 fn test_filter_and_then_chained() {
2199 let f1 = Filter::Equals("a".into(), "1".into());
2200 let f2 = Filter::Equals("b".into(), "2".into());
2201 let f3 = Filter::Equals("c".into(), "3".into());
2202 let combined = f1.and_then(f2).and_then(f3);
2203
2204 let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2205 assert!(sql.contains("AND"));
2206 assert_eq!(params.len(), 3);
2207 }
2208
2209 #[test]
2210 fn test_filter_or_else() {
2211 let f1 = Filter::Equals("status".into(), "active".into());
2212 let f2 = Filter::Equals("status".into(), "pending".into());
2213 let combined = f1.or_else(f2);
2214
2215 let (sql, _) = combined.to_sql(0, &crate::dialect::Postgres);
2216 assert!(sql.contains("OR"));
2217 }
2218
2219 #[test]
2220 fn test_filter_or_else_with_none_first() {
2221 let f1 = Filter::None;
2222 let f2 = Filter::Equals("name".into(), "Bob".into());
2223 let combined = f1.or_else(f2.clone());
2224 assert_eq!(combined, f2);
2225 }
2226
2227 #[test]
2228 fn test_filter_or_else_with_none_second() {
2229 let f1 = Filter::Equals("name".into(), "Alice".into());
2230 let f2 = Filter::None;
2231 let combined = f1.clone().or_else(f2);
2232 assert_eq!(combined, f1);
2233 }
2234
2235 #[test]
2238 fn test_filter_nested_and_or() {
2239 let f1 = Filter::Equals("status".into(), "active".into());
2240 let f2 = Filter::and([
2241 Filter::Gt("age".into(), FilterValue::Int(18)),
2242 Filter::Lt("age".into(), FilterValue::Int(65)),
2243 ]);
2244 let combined = Filter::and([f1, f2]);
2245
2246 let (sql, params) = combined.to_sql(0, &crate::dialect::Postgres);
2247 assert!(sql.contains("AND"));
2248 assert_eq!(params.len(), 3);
2249 }
2250
2251 #[test]
2252 fn test_filter_nested_not() {
2253 let inner = Filter::and([
2254 Filter::Equals("status".into(), "deleted".into()),
2255 Filter::Equals("archived".into(), FilterValue::Bool(true)),
2256 ]);
2257 let filter = Filter::not(inner);
2258
2259 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2260 assert!(sql.contains("NOT"));
2261 assert!(sql.contains("AND"));
2262 assert_eq!(params.len(), 2);
2263 }
2264
2265 #[test]
2266 fn test_filter_with_json_value() {
2267 let json_val = serde_json::json!({"key": "value"});
2268 let filter = Filter::Equals("metadata".into(), FilterValue::Json(json_val));
2269 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2270 assert_eq!(sql, r#""metadata" = $1"#);
2271 assert_eq!(params.len(), 1);
2272 }
2273
2274 #[test]
2275 fn test_filter_in_empty_list() {
2276 let filter = Filter::In("status".into(), vec![]);
2277 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2278 assert!(
2280 sql.contains("FALSE")
2281 || sql.contains("1=0")
2282 || sql.is_empty()
2283 || sql.contains("status")
2284 );
2285 assert!(params.is_empty());
2286 }
2287
2288 #[test]
2289 fn test_filter_with_null_value() {
2290 let filter = Filter::IsNull("deleted_at".into());
2292 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2293 assert!(sql.contains("deleted_at"));
2294 assert!(sql.contains("IS NULL"));
2295 assert!(params.is_empty());
2296 }
2297
2298 #[test]
2301 fn test_and_builder_basic() {
2302 let filter = Filter::and_builder(3)
2303 .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2304 .push(Filter::Gt("score".into(), FilterValue::Int(100)))
2305 .push(Filter::IsNotNull("email".into()))
2306 .build();
2307
2308 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2309 assert!(sql.contains("AND"));
2310 assert_eq!(params.len(), 2); }
2312
2313 #[test]
2314 fn test_and_builder_empty() {
2315 let filter = Filter::and_builder(0).build();
2316 assert!(filter.is_none());
2317 }
2318
2319 #[test]
2320 fn test_and_builder_single() {
2321 let filter = Filter::and_builder(1)
2322 .push(Filter::Equals("id".into(), FilterValue::Int(42)))
2323 .build();
2324
2325 assert!(matches!(filter, Filter::Equals(_, _)));
2327 }
2328
2329 #[test]
2330 fn test_and_builder_filters_none() {
2331 let filter = Filter::and_builder(3)
2332 .push(Filter::None)
2333 .push(Filter::Equals("id".into(), FilterValue::Int(1)))
2334 .push(Filter::None)
2335 .build();
2336
2337 assert!(matches!(filter, Filter::Equals(_, _)));
2339 }
2340
2341 #[test]
2342 fn test_and_builder_push_if() {
2343 let include_deleted = false;
2344 let filter = Filter::and_builder(2)
2345 .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2346 .push_if(include_deleted, Filter::IsNull("deleted_at".into()))
2347 .build();
2348
2349 assert!(matches!(filter, Filter::Equals(_, _)));
2351 }
2352
2353 #[test]
2354 fn test_or_builder_basic() {
2355 let filter = Filter::or_builder(2)
2356 .push(Filter::Equals(
2357 "role".into(),
2358 FilterValue::String("admin".into()),
2359 ))
2360 .push(Filter::Equals(
2361 "role".into(),
2362 FilterValue::String("moderator".into()),
2363 ))
2364 .build();
2365
2366 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2367 assert!(sql.contains("OR"));
2368 }
2369
2370 #[test]
2371 fn test_or_builder_empty() {
2372 let filter = Filter::or_builder(0).build();
2373 assert!(filter.is_none());
2374 }
2375
2376 #[test]
2377 fn test_or_builder_single() {
2378 let filter = Filter::or_builder(1)
2379 .push(Filter::Equals("id".into(), FilterValue::Int(42)))
2380 .build();
2381
2382 assert!(matches!(filter, Filter::Equals(_, _)));
2384 }
2385
2386 #[test]
2387 fn test_fluent_builder_and() {
2388 let filter = Filter::builder()
2389 .eq("status", "active")
2390 .gt("age", 18)
2391 .is_not_null("email")
2392 .build_and();
2393
2394 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2395 assert!(sql.contains("AND"));
2396 assert_eq!(params.len(), 2);
2397 }
2398
2399 #[test]
2400 fn test_fluent_builder_or() {
2401 let filter = Filter::builder()
2402 .eq("role", "admin")
2403 .eq("role", "moderator")
2404 .build_or();
2405
2406 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2407 assert!(sql.contains("OR"));
2408 }
2409
2410 #[test]
2411 fn test_fluent_builder_with_capacity() {
2412 let filter = Filter::builder()
2413 .with_capacity(5)
2414 .eq("a", 1)
2415 .ne("b", 2)
2416 .lt("c", 3)
2417 .lte("d", 4)
2418 .gte("e", 5)
2419 .build_and();
2420
2421 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2422 assert!(sql.contains("AND"));
2423 assert_eq!(params.len(), 5);
2424 }
2425
2426 #[test]
2427 fn test_fluent_builder_string_operations() {
2428 let filter = Filter::builder()
2429 .contains("name", "john")
2430 .starts_with("email", "admin")
2431 .ends_with("domain", ".com")
2432 .build_and();
2433
2434 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2435 assert!(sql.contains("LIKE"));
2436 }
2437
2438 #[test]
2439 fn test_fluent_builder_null_operations() {
2440 let filter = Filter::builder()
2441 .is_null("deleted_at")
2442 .is_not_null("created_at")
2443 .build_and();
2444
2445 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2446 assert!(sql.contains("IS NULL"));
2447 assert!(sql.contains("IS NOT NULL"));
2448 }
2449
2450 #[test]
2451 fn test_fluent_builder_in_operations() {
2452 let filter = Filter::builder()
2453 .is_in("status", vec!["pending", "processing"])
2454 .not_in("role", vec!["banned", "suspended"])
2455 .build_and();
2456
2457 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2458 assert!(sql.contains("IN"));
2459 assert!(sql.contains("NOT IN"));
2460 }
2461
2462 #[test]
2463 fn test_fluent_builder_filter_if() {
2464 let include_archived = false;
2465 let filter = Filter::builder()
2466 .eq("active", true)
2467 .filter_if(
2468 include_archived,
2469 Filter::Equals("archived".into(), FilterValue::Bool(true)),
2470 )
2471 .build_and();
2472
2473 assert!(matches!(filter, Filter::Equals(_, _)));
2475 }
2476
2477 #[test]
2478 fn test_fluent_builder_filter_if_some() {
2479 let maybe_status: Option<Filter> = Some(Filter::Equals("status".into(), "active".into()));
2480 let filter = Filter::builder()
2481 .eq("id", 1)
2482 .filter_if_some(maybe_status)
2483 .build_and();
2484
2485 assert!(matches!(filter, Filter::And(_)));
2486 }
2487
2488 #[test]
2489 fn test_and_builder_extend() {
2490 let extra_filters = vec![
2491 Filter::Gt("score".into(), FilterValue::Int(100)),
2492 Filter::Lt("score".into(), FilterValue::Int(1000)),
2493 ];
2494
2495 let filter = Filter::and_builder(3)
2496 .push(Filter::Equals("active".into(), FilterValue::Bool(true)))
2497 .extend(extra_filters)
2498 .build();
2499
2500 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2501 assert!(sql.contains("AND"));
2502 assert_eq!(params.len(), 3);
2503 }
2504
2505 #[test]
2506 fn test_builder_len_and_is_empty() {
2507 let mut builder = AndFilterBuilder::new();
2508 assert!(builder.is_empty());
2509 assert_eq!(builder.len(), 0);
2510
2511 builder = builder.push(Filter::Equals("id".into(), FilterValue::Int(1)));
2512 assert!(!builder.is_empty());
2513 assert_eq!(builder.len(), 1);
2514 }
2515
2516 #[test]
2519 fn test_and2_both_valid() {
2520 let a = Filter::Equals("id".into(), FilterValue::Int(1));
2521 let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2522 let filter = Filter::and2(a, b);
2523
2524 assert!(matches!(filter, Filter::And(_)));
2525 let (sql, params) = filter.to_sql(0, &crate::dialect::Postgres);
2526 assert!(sql.contains("AND"));
2527 assert_eq!(params.len(), 2);
2528 }
2529
2530 #[test]
2531 fn test_and2_first_none() {
2532 let a = Filter::None;
2533 let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2534 let filter = Filter::and2(a, b.clone());
2535
2536 assert_eq!(filter, b);
2537 }
2538
2539 #[test]
2540 fn test_and2_second_none() {
2541 let a = Filter::Equals("id".into(), FilterValue::Int(1));
2542 let b = Filter::None;
2543 let filter = Filter::and2(a.clone(), b);
2544
2545 assert_eq!(filter, a);
2546 }
2547
2548 #[test]
2549 fn test_and2_both_none() {
2550 let filter = Filter::and2(Filter::None, Filter::None);
2551 assert!(filter.is_none());
2552 }
2553
2554 #[test]
2555 fn test_or2_both_valid() {
2556 let a = Filter::Equals("role".into(), FilterValue::String("admin".into()));
2557 let b = Filter::Equals("role".into(), FilterValue::String("mod".into()));
2558 let filter = Filter::or2(a, b);
2559
2560 assert!(matches!(filter, Filter::Or(_)));
2561 let (sql, _) = filter.to_sql(0, &crate::dialect::Postgres);
2562 assert!(sql.contains("OR"));
2563 }
2564
2565 #[test]
2566 fn test_or2_first_none() {
2567 let a = Filter::None;
2568 let b = Filter::Equals("active".into(), FilterValue::Bool(true));
2569 let filter = Filter::or2(a, b.clone());
2570
2571 assert_eq!(filter, b);
2572 }
2573
2574 #[test]
2575 fn test_or2_second_none() {
2576 let a = Filter::Equals("id".into(), FilterValue::Int(1));
2577 let b = Filter::None;
2578 let filter = Filter::or2(a.clone(), b);
2579
2580 assert_eq!(filter, a);
2581 }
2582
2583 #[test]
2584 fn test_or2_both_none() {
2585 let filter = Filter::or2(Filter::None, Filter::None);
2586 assert!(filter.is_none());
2587 }
2588
2589 #[test]
2592 fn to_sql_quotes_column_names_against_injection() {
2593 use crate::dialect::{Mssql, Mysql, Postgres};
2594
2595 let filter = Filter::Equals(r#"id" OR 1=1--"#.into(), FilterValue::Int(1));
2597
2598 let (sql_pg, _) = filter.to_sql(0, &Postgres);
2599 assert!(
2600 sql_pg.starts_with(r#""id"" OR 1=1--" ="#),
2601 "postgres did not quote col; got: {sql_pg}"
2602 );
2603
2604 let (sql_my, _) = filter.to_sql(0, &Mysql);
2605 assert!(
2606 sql_my.starts_with(r#"`id" OR 1=1--` ="#),
2607 "mysql did not quote col; got: {sql_my}"
2608 );
2609
2610 let (sql_ms, _) = filter.to_sql(0, &Mssql);
2611 assert!(
2612 sql_ms.starts_with(r#"[id" OR 1=1--] ="#),
2613 "mssql did not quote col; got: {sql_ms}"
2614 );
2615 }
2616
2617 #[test]
2618 fn to_sql_quotes_in_list_column_names() {
2619 use crate::dialect::Postgres;
2620 let filter = Filter::In("id".into(), vec![FilterValue::Int(1), FilterValue::Int(2)]);
2621 let (sql, _) = filter.to_sql(0, &Postgres);
2622 assert!(
2623 sql.starts_with(r#""id" IN ("#),
2624 "expected quoted id on IN, got: {sql}"
2625 );
2626 }
2627
2628 #[test]
2629 fn to_sql_emits_sequential_placeholders() {
2630 use crate::dialect::Postgres;
2635
2636 let f = Filter::In(
2638 "id".into(),
2639 vec![
2640 FilterValue::Int(1),
2641 FilterValue::Int(2),
2642 FilterValue::Int(3),
2643 ],
2644 );
2645 let (sql, params) = f.to_sql(0, &Postgres);
2646 assert_eq!(sql, r#""id" IN ($1, $2, $3)"#, "IN placeholders: {sql}");
2647 assert_eq!(params.len(), 3);
2648
2649 let f = Filter::And(Box::new([
2651 Filter::Equals("a".into(), FilterValue::Int(10)),
2652 Filter::Equals("b".into(), FilterValue::Int(20)),
2653 ]));
2654 let (sql, params) = f.to_sql(0, &Postgres);
2655 assert_eq!(sql, r#"("a" = $1 AND "b" = $2)"#, "AND placeholders: {sql}");
2656 assert_eq!(params.len(), 2);
2657
2658 let f = Filter::And(Box::new([
2660 Filter::Equals("a".into(), FilterValue::Int(1)),
2661 Filter::In("b".into(), vec![FilterValue::Int(2), FilterValue::Int(3)]),
2662 ]));
2663 let (sql, params) = f.to_sql(0, &Postgres);
2664 assert_eq!(
2665 sql, r#"("a" = $1 AND "b" IN ($2, $3))"#,
2666 "mixed placeholders: {sql}"
2667 );
2668 assert_eq!(params.len(), 3);
2669
2670 let f = Filter::Equals("a".into(), FilterValue::Int(7));
2673 let (sql, _) = f.to_sql(5, &Postgres);
2674 assert_eq!(sql, r#""a" = $6"#, "offset placeholder: {sql}");
2675
2676 let f = Filter::NotIn(
2678 "status".into(),
2679 vec![
2680 FilterValue::String("deleted".into()),
2681 FilterValue::String("archived".into()),
2682 ],
2683 );
2684 let (sql, params) = f.to_sql(0, &Postgres);
2685 assert_eq!(
2686 sql, r#""status" NOT IN ($1, $2)"#,
2687 "NotIn placeholders: {sql}"
2688 );
2689 assert_eq!(params.len(), 2);
2690
2691 let f = Filter::Or(Box::new([
2693 Filter::Equals("a".into(), FilterValue::Int(100)),
2694 Filter::Equals("b".into(), FilterValue::Int(200)),
2695 ]));
2696 let (sql, params) = f.to_sql(0, &Postgres);
2697 assert_eq!(sql, r#"("a" = $1 OR "b" = $2)"#, "OR placeholders: {sql}");
2698 assert_eq!(params.len(), 2);
2699
2700 let f = Filter::StartsWith("name".into(), "admin".into());
2702 let (sql, params) = f.to_sql(0, &Postgres);
2703 assert_eq!(
2704 sql, r#""name" LIKE $1 ESCAPE '\'"#,
2705 "StartsWith placeholder: {sql}"
2706 );
2707 assert_eq!(params.len(), 1);
2708
2709 let f = Filter::And(Box::new([
2712 Filter::And(Box::new([
2713 Filter::Equals("a".into(), FilterValue::Int(1)),
2714 Filter::Equals("b".into(), FilterValue::Int(2)),
2715 ])),
2716 Filter::Equals("c".into(), FilterValue::Int(3)),
2717 ]));
2718 let (sql, params) = f.to_sql(0, &Postgres);
2719 assert_eq!(
2720 sql, r#"(("a" = $1 AND "b" = $2) AND "c" = $3)"#,
2721 "nested AND placeholders: {sql}"
2722 );
2723 assert_eq!(params.len(), 3);
2724
2725 let f = Filter::And(Box::new([
2727 Filter::Equals("a".into(), FilterValue::Int(10)),
2728 Filter::Or(Box::new([
2729 Filter::Equals("b".into(), FilterValue::Int(20)),
2730 Filter::Equals("c".into(), FilterValue::Int(30)),
2731 ])),
2732 ]));
2733 let (sql, params) = f.to_sql(0, &Postgres);
2734 assert_eq!(
2735 sql, r#"("a" = $1 AND ("b" = $2 OR "c" = $3))"#,
2736 "And-Or nesting: {sql}"
2737 );
2738 assert_eq!(params.len(), 3);
2739
2740 let f = Filter::In(
2742 "id".into(),
2743 vec![
2744 FilterValue::Int(1),
2745 FilterValue::Int(2),
2746 FilterValue::Int(3),
2747 ],
2748 );
2749 let (sql, params) = f.to_sql(5, &Postgres);
2750 assert_eq!(sql, r#""id" IN ($6, $7, $8)"#, "IN offset: {sql}");
2751 assert_eq!(params.len(), 3);
2752 }
2753
2754 #[test]
2755 fn to_sql_placeholders_are_dialect_specific() {
2756 use crate::dialect::{Mysql, Sqlite};
2759
2760 let in_filter = || {
2761 Filter::In(
2762 "id".into(),
2763 vec![
2764 FilterValue::Int(1),
2765 FilterValue::Int(2),
2766 FilterValue::Int(3),
2767 ],
2768 )
2769 };
2770
2771 let (sql, params) = in_filter().to_sql(0, &Sqlite);
2773 assert_eq!(sql, r#""id" IN (?1, ?2, ?3)"#, "SQLite IN: {sql}");
2774 assert_eq!(params.len(), 3);
2775
2776 let (sql, params) = in_filter().to_sql(0, &Mysql);
2778 assert_eq!(sql, "`id` IN (?, ?, ?)", "MySQL IN: {sql}");
2779 assert_eq!(params.len(), 3);
2780
2781 let f = Filter::And(Box::new([
2783 Filter::Equals("a".into(), FilterValue::Int(10)),
2784 Filter::Equals("b".into(), FilterValue::Int(20)),
2785 ]));
2786 let (sql, _) = f.to_sql(0, &Sqlite);
2787 assert_eq!(sql, r#"("a" = ?1 AND "b" = ?2)"#, "SQLite AND: {sql}");
2788 }
2789
2790 #[test]
2791 fn to_sql_quotes_null_checks() {
2792 use crate::dialect::Postgres;
2793 let filter = Filter::IsNull("deleted_at".into());
2794 let (sql, _) = filter.to_sql(0, &Postgres);
2795 assert_eq!(sql, r#""deleted_at" IS NULL"#);
2796 }
2797
2798 #[test]
2799 fn to_sql_quotes_comparison_operators() {
2800 use crate::dialect::Postgres;
2801
2802 let filter = Filter::Lt("age".into(), FilterValue::Int(18));
2803 let (sql, _) = filter.to_sql(0, &Postgres);
2804 assert!(sql.starts_with(r#""age" < "#), "Lt not quoted: {sql}");
2805
2806 let filter = Filter::Lte("price".into(), FilterValue::Int(100));
2807 let (sql, _) = filter.to_sql(0, &Postgres);
2808 assert!(sql.starts_with(r#""price" <= "#), "Lte not quoted: {sql}");
2809
2810 let filter = Filter::Gt("score".into(), FilterValue::Int(0));
2811 let (sql, _) = filter.to_sql(0, &Postgres);
2812 assert!(sql.starts_with(r#""score" > "#), "Gt not quoted: {sql}");
2813
2814 let filter = Filter::Gte("quantity".into(), FilterValue::Int(1));
2815 let (sql, _) = filter.to_sql(0, &Postgres);
2816 assert!(
2817 sql.starts_with(r#""quantity" >= "#),
2818 "Gte not quoted: {sql}"
2819 );
2820
2821 let filter = Filter::NotEquals("status".into(), "deleted".into());
2822 let (sql, _) = filter.to_sql(0, &Postgres);
2823 assert!(
2824 sql.starts_with(r#""status" != "#),
2825 "NotEquals not quoted: {sql}"
2826 );
2827 }
2828
2829 #[test]
2830 fn to_sql_quotes_like_operators() {
2831 use crate::dialect::Postgres;
2832
2833 let filter = Filter::Contains("email".into(), "example".into());
2834 let (sql, _) = filter.to_sql(0, &Postgres);
2835 assert!(
2836 sql.starts_with(r#""email" LIKE "#),
2837 "Contains not quoted: {sql}"
2838 );
2839
2840 let filter = Filter::StartsWith("name".into(), "admin".into());
2841 let (sql, _) = filter.to_sql(0, &Postgres);
2842 assert!(
2843 sql.starts_with(r#""name" LIKE "#),
2844 "StartsWith not quoted: {sql}"
2845 );
2846
2847 let filter = Filter::EndsWith("domain".into(), ".com".into());
2848 let (sql, _) = filter.to_sql(0, &Postgres);
2849 assert!(
2850 sql.starts_with(r#""domain" LIKE "#),
2851 "EndsWith not quoted: {sql}"
2852 );
2853 }
2854
2855 #[test]
2856 fn to_sql_quotes_not_in() {
2857 use crate::dialect::Postgres;
2858 let filter = Filter::NotIn("status".into(), vec!["deleted".into(), "archived".into()]);
2859 let (sql, _) = filter.to_sql(0, &Postgres);
2860 assert!(
2861 sql.starts_with(r#""status" NOT IN ("#),
2862 "NotIn not quoted: {sql}"
2863 );
2864 }
2865
2866 #[test]
2867 fn to_sql_quotes_is_not_null() {
2868 use crate::dialect::Postgres;
2869 let filter = Filter::IsNotNull("verified_at".into());
2870 let (sql, _) = filter.to_sql(0, &Postgres);
2871 assert_eq!(sql, r#""verified_at" IS NOT NULL"#);
2872 }
2873
2874 #[test]
2875 fn filter_value_from_u64_in_range() {
2876 assert_eq!(FilterValue::from(42u64), FilterValue::Int(42));
2877 assert_eq!(FilterValue::from(0u64), FilterValue::Int(0));
2878 let max_safe = i64::MAX as u64;
2879 assert_eq!(FilterValue::from(max_safe), FilterValue::Int(i64::MAX));
2880 }
2881
2882 #[test]
2883 #[should_panic(expected = "u64 value exceeds i64::MAX")]
2884 fn filter_value_from_u64_overflow_panics() {
2885 let _ = FilterValue::from(u64::MAX);
2886 }
2887
2888 #[test]
2889 fn filter_value_from_chrono_datetime_utc_rfc3339() {
2890 use chrono::{TimeZone, Utc};
2891 let dt = Utc.with_ymd_and_hms(2020, 1, 15, 10, 30, 45).unwrap();
2892 let fv = FilterValue::from(dt);
2893 assert_eq!(
2894 fv,
2895 FilterValue::String("2020-01-15T10:30:45.000000Z".to_string())
2896 );
2897 }
2898
2899 #[test]
2900 fn filter_value_from_chrono_naive_datetime_iso() {
2901 use chrono::NaiveDate;
2902 let dt = NaiveDate::from_ymd_opt(2020, 1, 15)
2903 .unwrap()
2904 .and_hms_opt(10, 30, 45)
2905 .unwrap();
2906 let fv = FilterValue::from(dt);
2907 assert_eq!(
2908 fv,
2909 FilterValue::String("2020-01-15T10:30:45.000000".to_string())
2910 );
2911 }
2912
2913 #[test]
2914 fn filter_value_from_chrono_naive_date() {
2915 use chrono::NaiveDate;
2916 let d = NaiveDate::from_ymd_opt(2020, 1, 15).unwrap();
2917 assert_eq!(
2918 FilterValue::from(d),
2919 FilterValue::String("2020-01-15".to_string())
2920 );
2921 }
2922
2923 #[test]
2924 fn filter_value_from_chrono_naive_time() {
2925 use chrono::NaiveTime;
2926 let t = NaiveTime::from_hms_opt(10, 30, 45).unwrap();
2927 assert_eq!(
2928 FilterValue::from(t),
2929 FilterValue::String("10:30:45.000000".to_string())
2930 );
2931 }
2932
2933 #[test]
2940 fn filter_value_from_uuid_is_lowercase_hyphenated() {
2941 use uuid::Uuid;
2946 let u = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
2947 match FilterValue::from(u) {
2948 FilterValue::String(ref s) => {
2949 assert_eq!(s, "550e8400-e29b-41d4-a716-446655440000");
2950 assert_eq!(s, &u.to_string());
2951 }
2952 other => panic!("expected FilterValue::String, got {other:?}"),
2953 }
2954 }
2955
2956 #[test]
2957 fn filter_value_from_uuid_nil_round_trips() {
2958 use uuid::Uuid;
2959 let u = Uuid::nil();
2960 assert_eq!(
2961 FilterValue::from(u),
2962 FilterValue::String("00000000-0000-0000-0000-000000000000".to_string())
2963 );
2964 }
2965
2966 #[test]
2967 fn filter_value_from_decimal_uses_to_string_not_f64() {
2968 use rust_decimal::Decimal;
2972 use std::str::FromStr;
2973 let d = Decimal::from_str("3.14").unwrap();
2974 assert_eq!(
2975 FilterValue::from(d),
2976 FilterValue::String("3.14".to_string())
2977 );
2978 }
2979
2980 #[test]
2981 fn filter_value_from_decimal_high_precision_preserved() {
2982 use rust_decimal::Decimal;
2983 use std::str::FromStr;
2984 let d = Decimal::from_str("1234567890.1234567890").unwrap();
2986 match FilterValue::from(d) {
2987 FilterValue::String(ref s) => {
2988 assert_eq!(s, "1234567890.1234567890");
2989 }
2990 other => panic!("expected FilterValue::String, got {other:?}"),
2991 }
2992 }
2993
2994 #[test]
2995 fn filter_value_from_serde_json_value_keeps_json_variant() {
2996 let v = serde_json::json!({"key": "value", "nested": [1, 2, 3]});
2997 match FilterValue::from(v.clone()) {
2998 FilterValue::Json(inner) => {
2999 assert_eq!(inner, v);
3000 }
3001 other => panic!("expected FilterValue::Json, got {other:?}"),
3002 }
3003 }
3004
3005 #[test]
3006 fn filter_value_from_serde_json_null_keeps_json_variant() {
3007 let v = serde_json::Value::Null;
3012 match FilterValue::from(v) {
3013 FilterValue::Json(serde_json::Value::Null) => {}
3014 other => panic!("expected FilterValue::Json(Null), got {other:?}"),
3015 }
3016 }
3017
3018 #[test]
3019 fn filter_value_from_option_none_maps_to_null() {
3020 let none_i32: Option<i32> = None;
3023 assert_eq!(FilterValue::from(none_i32), FilterValue::Null);
3024 let none_string: Option<String> = None;
3025 assert_eq!(FilterValue::from(none_string), FilterValue::Null);
3026 }
3027
3028 #[test]
3029 fn filter_value_from_signed_integer_extremes() {
3030 assert_eq!(FilterValue::from(i8::MIN), FilterValue::Int(i8::MIN as i64));
3033 assert_eq!(FilterValue::from(i8::MAX), FilterValue::Int(i8::MAX as i64));
3034 assert_eq!(
3035 FilterValue::from(i16::MIN),
3036 FilterValue::Int(i16::MIN as i64)
3037 );
3038 assert_eq!(
3039 FilterValue::from(i16::MAX),
3040 FilterValue::Int(i16::MAX as i64)
3041 );
3042 }
3043
3044 #[test]
3045 fn filter_value_from_unsigned_integer_extremes() {
3046 assert_eq!(FilterValue::from(u8::MAX), FilterValue::Int(u8::MAX as i64));
3049 assert_eq!(
3050 FilterValue::from(u16::MAX),
3051 FilterValue::Int(u16::MAX as i64)
3052 );
3053 assert_eq!(
3054 FilterValue::from(u32::MAX),
3055 FilterValue::Int(u32::MAX as i64)
3056 );
3057 assert_eq!(FilterValue::from(u32::MAX), FilterValue::Int(4_294_967_295));
3059 }
3060
3061 #[test]
3062 fn filter_value_from_f32_widens_to_f64() {
3063 let v: f32 = 1.5;
3068 assert_eq!(FilterValue::from(v), FilterValue::Float(1.5));
3069 }
3070
3071 #[test]
3078 fn to_filter_value_option_some_some() {
3079 let v: Option<i32> = Some(42);
3080 assert_eq!(v.to_filter_value(), FilterValue::Int(42));
3081 }
3082
3083 #[test]
3084 fn to_filter_value_option_none_is_null() {
3085 let v: Option<i32> = None;
3086 assert_eq!(v.to_filter_value(), FilterValue::Null);
3087 }
3088
3089 #[test]
3090 fn to_filter_value_uuid_is_string() {
3091 let id = uuid::Uuid::nil();
3092 assert_eq!(id.to_filter_value(), FilterValue::String(id.to_string()));
3093 }
3094
3095 #[test]
3096 fn to_filter_value_bool_is_bool() {
3097 assert_eq!(true.to_filter_value(), FilterValue::Bool(true));
3098 }
3099
3100 #[test]
3101 fn scalar_subquery_lowers_to_inline_sql_with_dialect_placeholders() {
3102 use crate::dialect::Postgres;
3103 let f = Filter::ScalarSubquery {
3104 sql: Cow::Borrowed(
3105 "(SELECT COUNT(*) FROM posts p WHERE p.author_id = users.id AND p.published = {0}) > {1}",
3106 ),
3107 params: vec![FilterValue::Bool(true), FilterValue::Int(5)],
3108 };
3109 let (sql, params) = f.to_sql(0, &Postgres);
3110 assert_eq!(
3111 sql,
3112 "((SELECT COUNT(*) FROM posts p WHERE p.author_id = users.id AND p.published = $1) > $2)"
3113 );
3114 assert_eq!(params, vec![FilterValue::Bool(true), FilterValue::Int(5)]);
3115 }
3116
3117 #[test]
3118 fn scalar_subquery_offsets_placeholders_inside_and() {
3119 use crate::dialect::Postgres;
3120 let f = Filter::and([
3121 Filter::Equals("active".into(), FilterValue::Bool(true)),
3122 Filter::ScalarSubquery {
3123 sql: Cow::Borrowed(
3124 "(SELECT COUNT(*) FROM posts p WHERE p.author_id = users.id) >= {0}",
3125 ),
3126 params: vec![FilterValue::Int(1)],
3127 },
3128 ]);
3129 let (sql, params) = f.to_sql(0, &Postgres);
3130 assert!(sql.contains("$1"));
3132 assert!(sql.contains("$2"));
3133 assert_eq!(params.len(), 2);
3134 assert_eq!(params[0], FilterValue::Bool(true));
3135 assert_eq!(params[1], FilterValue::Int(1));
3136 }
3137
3138 #[test]
3139 fn scalar_subquery_handles_out_of_order_and_repeated_placeholders() {
3140 use crate::dialect::Postgres;
3141 let f = Filter::ScalarSubquery {
3142 sql: Cow::Borrowed("{1} = {0} AND {1} > {0}"),
3143 params: vec![FilterValue::Int(1), FilterValue::Int(2)],
3144 };
3145 let (sql, params) = f.to_sql(0, &Postgres);
3146 assert_eq!(sql, "($2 = $1 AND $2 > $1)");
3149 assert_eq!(params, vec![FilterValue::Int(1), FilterValue::Int(2)]);
3150 }
3151}