query_lite/
lib.rs

1pub mod error;
2
3use error::{Error, Result};
4use indexmap::IndexMap;
5use std::str::FromStr;
6use url::form_urlencoded;
7
8pub const QUESTION: char = '?';
9pub const AMPERSAND: char = '&';
10pub const EQUAL: char = '=';
11pub const COLON: char = ':';
12pub const COMMA: char = ',';
13pub const PERCENT: char = '%';
14
15/// URL decode a string, handling percent-encoded characters
16pub fn url_decode(input: &str) -> String {
17    // Only decode if the string contains percent-encoded characters
18    if input.contains(PERCENT) {
19        // Use form_urlencoded to decode individual values by treating it as a query parameter
20        let query_str = format!("key={}", input);
21        form_urlencoded::parse(query_str.as_bytes())
22            .next()
23            .map(|(_, v)| v.to_string())
24            .unwrap_or_else(|| input.to_string())
25    } else {
26        input.to_string()
27    }
28}
29
30/// URL encode a string, converting special characters to percent-encoded format
31pub fn url_encode(input: &str) -> String {
32    form_urlencoded::byte_serialize(input.as_bytes()).collect()
33}
34
35/// Parse a parameter string into similarity and values
36///
37/// # Examples
38/// - "contains:damian" -> (Similarity::Contains, vec!["damian"])
39/// - "equals:black,steel,wood" -> (Similarity::Equals, vec!["black", "steel", "wood"])
40/// - "between:20,30" -> (Similarity::Between, vec!["20", "30"])
41pub fn parse_parameter(s: &str) -> Result<Parameter> {
42    let trimmed = s.trim();
43    if trimmed.is_empty() {
44        return Err(Error::InvalidParameter(s.into()));
45    }
46
47    let parts: Vec<&str> = trimmed.split(COLON).collect();
48    if parts.len() != 2 {
49        return Err(Error::InvalidParameter(s.into()));
50    }
51
52    let similarity_str = parts[0].trim();
53    let values_str = parts[1].trim();
54
55    if similarity_str.is_empty() {
56        return Err(Error::InvalidParameter(s.into()));
57    }
58
59    let values: Vec<String> = if values_str.is_empty() {
60        vec![]
61    } else {
62        values_str
63            .split(COMMA)
64            .map(|v| url_decode(v.trim()))
65            .filter(|v| !v.is_empty())
66            .collect()
67    };
68
69    let similarity = Similarity::from_str(similarity_str)?;
70    Ok(Parameter(similarity, values))
71}
72
73/// Parse a sort field string into name and order
74///
75/// # Examples
76/// - "name:asc" -> ("name", SortOrder::Ascending)
77/// - "date_created:desc" -> ("date_created", SortOrder::Descending)
78pub fn parse_sort_field(s: &str) -> Result<(String, SortOrder)> {
79    let trimmed = s.trim();
80    if trimmed.is_empty() {
81        return Err(Error::InvalidSortField(s.into()));
82    }
83
84    let parts: Vec<&str> = trimmed.split(COLON).collect();
85    if parts.len() != 2 {
86        return Err(Error::InvalidSortField(s.into()));
87    }
88
89    let name = url_decode(parts[0].trim());
90    let order_str = parts[1].trim();
91
92    if name.is_empty() || order_str.is_empty() {
93        return Err(Error::InvalidSortField(s.into()));
94    }
95
96    let order = SortOrder::from_str(order_str)?;
97    Ok((name, order))
98}
99
100#[derive(Clone, Debug, PartialEq)]
101pub enum SortOrder {
102    Ascending,
103    Descending,
104}
105
106impl SortOrder {
107    pub const ASCENDING: &str = "asc";
108    pub const DESCENDING: &str = "desc";
109}
110
111impl Default for SortOrder {
112    fn default() -> Self {
113        Self::Ascending
114    }
115}
116
117impl FromStr for SortOrder {
118    type Err = Error;
119    fn from_str(s: &str) -> Result<Self> {
120        match s {
121            SortOrder::ASCENDING => Ok(SortOrder::Ascending),
122            SortOrder::DESCENDING => Ok(SortOrder::Descending),
123            val => Err(Error::InvalidSortOrder(val.into())),
124        }
125    }
126}
127
128impl ToString for SortOrder {
129    fn to_string(&self) -> String {
130        match self {
131            Self::Ascending => SortOrder::ASCENDING.to_string(),
132            Self::Descending => SortOrder::DESCENDING.to_string(),
133        }
134    }
135}
136
137#[derive(Clone, Debug, PartialEq)]
138pub struct SortFields(pub IndexMap<String, SortOrder>);
139
140impl SortFields {
141    pub fn new() -> Self {
142        Self(IndexMap::new())
143    }
144
145    pub fn inner(&self) -> &IndexMap<String, SortOrder> {
146        &self.0
147    }
148
149    pub fn inner_mut(&mut self) -> &mut IndexMap<String, SortOrder> {
150        &mut self.0
151    }
152
153    pub fn ascending(&mut self, name: String) -> &mut Self {
154        self.0.insert(name, SortOrder::Ascending);
155        self
156    }
157
158    pub fn descending(&mut self, name: String) -> &mut Self {
159        self.0.insert(name, SortOrder::Descending);
160        self
161    }
162
163    pub fn keep(&self, keys: Vec<String>) -> Self {
164        let mut result = Self::new();
165        for key in keys {
166            if let Some(value) = self.0.get(&key) {
167                result.0.insert(key, value.clone());
168            }
169        }
170        result
171    }
172
173    pub fn remove(&self, keys: Vec<String>) -> Self {
174        let mut result = self.clone();
175        for key in keys {
176            result.0.shift_remove(&key);
177        }
178        result
179    }
180}
181
182impl Default for SortFields {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188impl FromStr for SortFields {
189    type Err = Error;
190
191    // EXAMPLE INPUT
192    // date_created:desc,name:asc,surname:asc
193    fn from_str(s: &str) -> Result<Self> {
194        let trimmed = s.trim();
195        if trimmed.is_empty() {
196            return Ok(SortFields::new());
197        }
198
199        let str_fields: Vec<&str> = trimmed.split(COMMA).collect();
200        let mut sort_fields: Self = SortFields(IndexMap::new());
201
202        for str_field in str_fields {
203            let trimmed_field = str_field.trim();
204            if trimmed_field.is_empty() {
205                continue;
206            }
207
208            let (name, order) = parse_sort_field(trimmed_field)?;
209            sort_fields.0.insert(name, order);
210        }
211
212        Ok(sort_fields)
213    }
214}
215
216#[derive(Clone, Debug, PartialEq)]
217pub enum Similarity {
218    Equals,
219    Contains,
220    StartsWith,
221    EndsWith,
222
223    Between,
224    Lesser,
225    LesserOrEqual,
226    Greater,
227    GreaterOrEqual,
228}
229
230impl Similarity {
231    pub const EQUALS: &str = "equals";
232    pub const CONTAINS: &str = "contains";
233    pub const STARTS_WITH: &str = "starts-with";
234    pub const ENDS_WITH: &str = "ends-with";
235
236    pub const BETWEEN: &str = "between";
237    pub const LESSER: &str = "lesser";
238    pub const LESSER_OR_EQUAL: &str = "lesser-or-equal";
239    pub const GREATER: &str = "greater";
240    pub const GREATER_OR_EQUAL: &str = "greater-or-equal";
241}
242
243impl Default for Similarity {
244    fn default() -> Self {
245        Self::Equals
246    }
247}
248
249impl FromStr for Similarity {
250    type Err = Error;
251    fn from_str(s: &str) -> Result<Self> {
252        match s {
253            Similarity::EQUALS => Ok(Similarity::Equals),
254            Similarity::CONTAINS => Ok(Similarity::Contains),
255            Similarity::STARTS_WITH => Ok(Similarity::StartsWith),
256            Similarity::ENDS_WITH => Ok(Similarity::EndsWith),
257
258            Similarity::BETWEEN => Ok(Similarity::Between),
259            Similarity::LESSER => Ok(Similarity::Lesser),
260            Similarity::LESSER_OR_EQUAL => Ok(Similarity::LesserOrEqual),
261            Similarity::GREATER => Ok(Similarity::Greater),
262            Similarity::GREATER_OR_EQUAL => Ok(Similarity::GreaterOrEqual),
263
264            val => Err(Error::InvalidSimilarity(val.into())),
265        }
266    }
267}
268
269impl ToString for Similarity {
270    fn to_string(&self) -> String {
271        match self {
272            Self::Equals => Self::EQUALS.to_string(),
273            Self::Contains => Self::CONTAINS.to_string(),
274            Self::StartsWith => Self::STARTS_WITH.to_string(),
275            Self::EndsWith => Self::ENDS_WITH.to_string(),
276
277            Self::Between => Self::BETWEEN.to_string(),
278            Self::Lesser => Self::LESSER.to_string(),
279            Self::LesserOrEqual => Self::LESSER_OR_EQUAL.to_string(),
280            Self::Greater => Self::GREATER.to_string(),
281            Self::GreaterOrEqual => Self::GREATER_OR_EQUAL.to_string(),
282        }
283    }
284}
285
286#[derive(Clone, Debug, PartialEq)]
287pub struct Parameter(pub Similarity, pub Vec<String>);
288
289impl Parameter {
290    pub fn similarity(&self) -> &Similarity {
291        &self.0
292    }
293
294    pub fn values(&self) -> &Vec<String> {
295        &self.1
296    }
297
298    pub fn values_mut(&mut self) -> &mut Vec<String> {
299        &mut self.1
300    }
301}
302
303#[derive(Clone, Debug, PartialEq)]
304pub struct Parameters(pub IndexMap<String, Parameter>);
305
306impl Parameters {
307    pub const ORDER: &str = "order";
308    pub const LIMIT: &str = "limit";
309    pub const OFFSET: &str = "offset";
310
311    pub const EXCLUDE: [&str; 3] = [Parameters::ORDER, Parameters::LIMIT, Parameters::OFFSET];
312
313    pub const DEFAULT_LIMIT: usize = 50;
314    pub const DEFAULT_OFFSET: usize = 0;
315
316    pub fn new() -> Self {
317        Self(IndexMap::new())
318    }
319
320    pub fn inner(&self) -> &IndexMap<String, Parameter> {
321        &self.0
322    }
323
324    pub fn inner_mut(&mut self) -> &mut IndexMap<String, Parameter> {
325        &mut self.0
326    }
327
328    pub fn equals(&mut self, key: String, values: Vec<String>) -> &mut Self {
329        self.0.insert(key, Parameter(Similarity::Equals, values));
330        self
331    }
332
333    pub fn contains(&mut self, key: String, values: Vec<String>) -> &mut Self {
334        self.0.insert(key, Parameter(Similarity::Contains, values));
335        self
336    }
337
338    pub fn starts_with(&mut self, key: String, values: Vec<String>) -> &mut Self {
339        self.0
340            .insert(key, Parameter(Similarity::StartsWith, values));
341        self
342    }
343
344    pub fn ends_with(&mut self, key: String, values: Vec<String>) -> &mut Self {
345        self.0.insert(key, Parameter(Similarity::EndsWith, values));
346        self
347    }
348
349    pub fn between(&mut self, key: String, values: Vec<String>) -> &mut Self {
350        self.0.insert(key, Parameter(Similarity::Between, values));
351        self
352    }
353
354    pub fn lesser(&mut self, key: String, values: Vec<String>) -> &mut Self {
355        self.0.insert(key, Parameter(Similarity::Lesser, values));
356        self
357    }
358
359    pub fn lesser_or_equal(&mut self, key: String, values: Vec<String>) -> &mut Self {
360        self.0
361            .insert(key, Parameter(Similarity::LesserOrEqual, values));
362        self
363    }
364
365    pub fn greater(&mut self, key: String, values: Vec<String>) -> &mut Self {
366        self.0.insert(key, Parameter(Similarity::Greater, values));
367        self
368    }
369
370    pub fn greater_or_equal(&mut self, key: String, values: Vec<String>) -> &mut Self {
371        self.0
372            .insert(key, Parameter(Similarity::GreaterOrEqual, values));
373        self
374    }
375
376    pub fn keep(&self, keys: Vec<String>) -> Self {
377        let mut result = Self::new();
378        for key in keys {
379            if let Some(value) = self.0.get(&key) {
380                result.0.insert(key, value.clone());
381            }
382        }
383        result
384    }
385
386    pub fn remove(&self, keys: Vec<String>) -> Self {
387        let mut result = self.clone();
388        for key in keys {
389            result.0.shift_remove(&key);
390        }
391        result
392    }
393}
394
395impl Default for Parameters {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401impl FromStr for Parameters {
402    type Err = Error;
403
404    // EXAMPLE INPUT
405    // name=contains:damian&surname=equals:black,steel,wood&order=date_created:desc&limit=40&offset=0
406    fn from_str(s: &str) -> Result<Self> {
407        let trimmed = s.trim();
408        if trimmed.is_empty() {
409            return Ok(Parameters::new());
410        }
411
412        let str_parameters: Vec<&str> = trimmed.split(AMPERSAND).collect();
413        let mut parameters: Self = Parameters(IndexMap::new());
414
415        for str_param in str_parameters {
416            let trimmed_param = str_param.trim();
417            if trimmed_param.is_empty() {
418                continue;
419            }
420
421            let mut parts = trimmed_param.splitn(2, EQUAL);
422            let (key, value) = match (parts.next(), parts.next()) {
423                (Some(k), Some(v)) => (k, v),
424                _ => return Err(Error::InvalidParameter(trimmed_param.into())),
425            };
426
427            let trimmed_key = key.trim();
428            if trimmed_key.is_empty() || Parameters::EXCLUDE.contains(&trimmed_key) {
429                continue;
430            }
431
432            let Parameter(similarity, values) = parse_parameter(value)?;
433            // Only add parameters that have values
434            if values.is_empty() {
435                continue;
436            }
437
438            parameters
439                .0
440                .insert(trimmed_key.to_string(), Parameter(similarity, values));
441        }
442
443        Ok(parameters)
444    }
445}
446
447#[derive(Clone, Debug, PartialEq)]
448pub enum SqlValue {
449    /// The value is a `NULL` value.
450    Null,
451    /// The value is a signed integer.
452    Integer(i64),
453    /// The value is a floating point number.
454    Real(f64),
455    /// The value is a text string.
456    Text(String),
457    /// The value is a blob of data
458    Blob(Vec<u8>),
459}
460
461#[derive(Clone, Debug, PartialEq)]
462pub struct Query {
463    pub parameters: Parameters,
464    pub sort_fields: SortFields,
465    pub limit: usize,
466    pub offset: usize,
467}
468
469impl Query {
470    pub fn new() -> Self {
471        Self {
472            parameters: Parameters::new(),
473            sort_fields: SortFields::new(),
474            limit: Parameters::DEFAULT_LIMIT,
475            offset: Parameters::DEFAULT_OFFSET,
476        }
477    }
478
479    pub fn init(
480        parameters: Parameters,
481        sort_fields: SortFields,
482        limit: usize,
483        offset: usize,
484    ) -> Self {
485        Self {
486            parameters,
487            sort_fields,
488            limit,
489            offset,
490        }
491    }
492
493    pub fn to_http(&self) -> String {
494        let mut params = self
495            .parameters
496            .0
497            .iter()
498            .filter(|(_, param)| param.values().len() > 0)
499            .map(|(key, param)| {
500                let similarity_str = param.similarity().to_string();
501                let values_str = param
502                    .values()
503                    .iter()
504                    .map(|v| url_encode(v))
505                    .collect::<Vec<String>>()
506                    .join(&format!("{COMMA}"));
507                format!("{key}{EQUAL}{similarity_str}{COLON}{values_str}",)
508            })
509            .collect::<Vec<String>>()
510            .join("&");
511
512        let order = self
513            .sort_fields
514            .0
515            .iter()
516            .filter(|(name, _)| name.len() > 0)
517            .map(|(name, order)| format!("{name}{COLON}{}", order.to_string()))
518            .collect::<Vec<String>>()
519            .join(&format!("{COMMA}"));
520
521        if params.len() > 0 {
522            params.push_str(&format!("{AMPERSAND}"));
523        }
524
525        if order.len() > 0 {
526            params.push_str(&format!("{}{EQUAL}{}", Parameters::ORDER, order));
527            params.push_str(&format!("{AMPERSAND}"));
528        }
529
530        format!(
531            "{params}{}{EQUAL}{}{AMPERSAND}{}{EQUAL}{}",
532            Parameters::LIMIT,
533            self.limit,
534            Parameters::OFFSET,
535            self.offset,
536        )
537    }
538
539    // name=contains:damian&surname=equals:black,steel,wood&order=date_created:desc&limit=40&offset=0
540    pub fn from_http(search: String) -> Result<Self> {
541        let mut query = Self::new();
542        let trimmed_search = search.trim_start_matches(QUESTION).trim();
543
544        if trimmed_search.is_empty() {
545            return Ok(query);
546        }
547
548        for k_v in trimmed_search.split(AMPERSAND) {
549            let trimmed_kv = k_v.trim();
550            if trimmed_kv.is_empty() {
551                continue;
552            }
553
554            let mut parts = trimmed_kv.splitn(2, EQUAL);
555            if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
556                let trimmed_key = key.trim();
557                let trimmed_value = value.trim();
558
559                if trimmed_key.is_empty() || trimmed_value.is_empty() {
560                    continue;
561                }
562
563                match trimmed_key {
564                    Parameters::ORDER => {
565                        // Check if the value looks like a sort field format (contains colon)
566                        if !trimmed_value.contains(COLON) {
567                            // Fail on clearly invalid formats (like "invalid")
568                            return Err(Error::InvalidSortField(trimmed_value.into()));
569                        }
570
571                        if let Ok(sort_fields) = SortFields::from_str(trimmed_value) {
572                            query.sort_fields = sort_fields;
573                        }
574                        // Skip malformed sort fields (like ":desc")
575                    }
576                    Parameters::LIMIT => {
577                        query.limit = trimmed_value.parse().unwrap_or(Parameters::DEFAULT_LIMIT);
578                    }
579                    Parameters::OFFSET => {
580                        query.offset = trimmed_value.parse().unwrap_or(Parameters::DEFAULT_OFFSET);
581                    }
582                    _k => {
583                        // Check if this is a similarity-based parameter (contains colon)
584                        if trimmed_value.contains(COLON) {
585                            // Parse as similarity-based parameter
586                            let Parameter(similarity, values) = parse_parameter(trimmed_value)?;
587                            // Only add parameters that have values
588                            if values.is_empty() {
589                                continue;
590                            }
591                            // Replace any existing parameter (similarity-based takes precedence)
592                            query
593                                .parameters
594                                .0
595                                .insert(trimmed_key.to_string(), Parameter(similarity, values));
596                        } else {
597                            // Handle as normal query parameter (default to equals similarity)
598                            let decoded_value = url_decode(trimmed_value);
599
600                            // Check if parameter already exists and is not similarity-based
601                            if let Some(existing_param) =
602                                query.parameters.0.get_mut(&trimmed_key.to_string())
603                            {
604                                // Only append if the existing parameter is also equals similarity
605                                if *existing_param.similarity() == Similarity::Equals {
606                                    existing_param.1.push(decoded_value);
607                                }
608                                // If existing parameter is similarity-based, ignore this normal parameter
609                            } else {
610                                // Create new parameter with equals similarity
611                                query.parameters.0.insert(
612                                    trimmed_key.to_string(),
613                                    Parameter(Similarity::Equals, vec![decoded_value]),
614                                );
615                            }
616                        }
617                    }
618                }
619            } else {
620                return Err(Error::InvalidSearchParameters(search));
621            }
622        }
623
624        Ok(query)
625    }
626
627    #[cfg(feature = "sql")]
628    pub fn to_sql(&self) -> String {
629        let mut sql_parts = Vec::new();
630
631        // Build WHERE clause from parameters
632        let where_clause = self.where_clause();
633        if !where_clause.is_empty() {
634            sql_parts.push(format!("WHERE {}", where_clause));
635        }
636
637        // Build ORDER BY clause from sort fields
638        let order_clause = self.order_clause();
639        if !order_clause.is_empty() {
640            sql_parts.push(format!("ORDER BY {}", order_clause));
641        }
642
643        // Add LIMIT and OFFSET
644        sql_parts.push(format!("LIMIT ? OFFSET ?"));
645
646        sql_parts.join(" ")
647    }
648
649    #[cfg(feature = "sql")]
650    pub fn where_clause(&self) -> String {
651        let mut conditions = Vec::new();
652
653        for (key, param) in &self.parameters.0 {
654            let similarity = param.similarity();
655            let values = param.values();
656            if values.is_empty() {
657                continue;
658            }
659
660            let condition = match similarity {
661                Similarity::Equals => {
662                    if values.len() == 1 {
663                        if values[0] == "null" {
664                            format!("{} IS ?", key)
665                        } else {
666                            format!("{} = ?", key)
667                        }
668                    } else {
669                        let placeholders = vec!["?"; values.len()].join(", ");
670                        format!("{} IN ({})", key, placeholders)
671                    }
672                }
673                Similarity::Contains => {
674                    if values.len() == 1 {
675                        format!("{} LIKE ?", key)
676                    } else {
677                        let like_conditions: Vec<String> =
678                            values.iter().map(|_| format!("{} LIKE ?", key)).collect();
679                        format!("({})", like_conditions.join(" OR "))
680                    }
681                }
682                Similarity::StartsWith => {
683                    if values.len() == 1 {
684                        format!("{} LIKE ?", key)
685                    } else {
686                        let like_conditions: Vec<String> =
687                            values.iter().map(|_| format!("{} LIKE ?", key)).collect();
688                        format!("({})", like_conditions.join(" OR "))
689                    }
690                }
691                Similarity::EndsWith => {
692                    if values.len() == 1 {
693                        format!("{} LIKE ?", key)
694                    } else {
695                        let like_conditions: Vec<String> =
696                            values.iter().map(|_| format!("{} LIKE ?", key)).collect();
697                        format!("({})", like_conditions.join(" OR "))
698                    }
699                }
700                Similarity::Between => {
701                    if values.len() >= 2 {
702                        // Group values into pairs, ignoring any odd value
703                        let pairs: Vec<&[String]> = values.chunks(2).collect();
704                        let between_conditions: Vec<String> = pairs
705                            .iter()
706                            .map(|pair| {
707                                if pair.len() == 2 {
708                                    format!("{} BETWEEN ? AND ?", key)
709                                } else {
710                                    String::new() // Skip incomplete pairs
711                                }
712                            })
713                            .filter(|condition| !condition.is_empty())
714                            .collect();
715
716                        if between_conditions.is_empty() {
717                            continue; // Skip if no valid pairs
718                        } else if between_conditions.len() == 1 {
719                            between_conditions[0].clone()
720                        } else {
721                            format!("({})", between_conditions.join(" OR "))
722                        }
723                    } else {
724                        continue; // Skip invalid between conditions
725                    }
726                }
727                Similarity::Lesser => {
728                    if values.len() == 1 {
729                        format!("{} < ?", key)
730                    } else {
731                        let conditions: Vec<String> =
732                            values.iter().map(|_| format!("{} < ?", key)).collect();
733                        format!("({})", conditions.join(" OR "))
734                    }
735                }
736                Similarity::LesserOrEqual => {
737                    if values.len() == 1 {
738                        format!("{} <= ?", key)
739                    } else {
740                        let conditions: Vec<String> =
741                            values.iter().map(|_| format!("{} <= ?", key)).collect();
742                        format!("({})", conditions.join(" OR "))
743                    }
744                }
745                Similarity::Greater => {
746                    if values.len() == 1 {
747                        format!("{} > ?", key)
748                    } else {
749                        let conditions: Vec<String> =
750                            values.iter().map(|_| format!("{} > ?", key)).collect();
751                        format!("({})", conditions.join(" OR "))
752                    }
753                }
754                Similarity::GreaterOrEqual => {
755                    if values.len() == 1 {
756                        format!("{} >= ?", key)
757                    } else {
758                        let conditions: Vec<String> =
759                            values.iter().map(|_| format!("{} >= ?", key)).collect();
760                        format!("({})", conditions.join(" OR "))
761                    }
762                }
763            };
764
765            conditions.push(condition);
766        }
767
768        conditions.join(" AND ")
769    }
770
771    #[cfg(feature = "sql")]
772    pub fn order_clause(&self) -> String {
773        let mut order_parts = Vec::new();
774
775        for (name, order) in &self.sort_fields.0 {
776            if !name.is_empty() {
777                let direction = match order {
778                    SortOrder::Ascending => "ASC",
779                    SortOrder::Descending => "DESC",
780                };
781                order_parts.push(format!("{} {}", name, direction));
782            }
783        }
784
785        order_parts.join(", ")
786    }
787
788    #[cfg(feature = "sql")]
789    pub fn to_values(&self) -> Vec<SqlValue> {
790        let mut sql_values = self.parameter_values();
791        sql_values.extend(self.pagination_values());
792        sql_values
793    }
794
795    #[cfg(feature = "sql")]
796    /// Get SQL values for parameters only (without limit and offset)
797    pub fn parameter_values(&self) -> Vec<SqlValue> {
798        let mut sql_values = Vec::new();
799
800        for (_k, param) in self.parameters.inner() {
801            let param_similarity = param.similarity();
802            let param_values = param.values();
803            for cur_val in param_values {
804                // Skip empty values
805                if cur_val.trim().is_empty() {
806                    continue;
807                }
808
809                if cur_val == "null" {
810                    sql_values.push(SqlValue::Null);
811                    continue;
812                }
813
814                let sql_value = match *param_similarity {
815                    Similarity::Contains => SqlValue::Text(format!("%{}%", cur_val)),
816                    Similarity::StartsWith => SqlValue::Text(format!("{}%", cur_val)),
817                    Similarity::EndsWith => SqlValue::Text(format!("%{}", cur_val)),
818                    _ => {
819                        // Try to parse as integer first, then float, then text
820                        if let Ok(i) = cur_val.parse::<i64>() {
821                            SqlValue::Integer(i)
822                        } else if let Ok(f) = cur_val.parse::<f64>() {
823                            SqlValue::Real(f)
824                        } else {
825                            SqlValue::Text(cur_val.clone())
826                        }
827                    }
828                };
829
830                sql_values.push(sql_value);
831            }
832        }
833
834        sql_values
835    }
836
837    #[cfg(feature = "sql")]
838    /// Get SQL values for pagination (limit and offset only)
839    pub fn pagination_values(&self) -> Vec<SqlValue> {
840        vec![
841            SqlValue::Integer(self.limit as i64),
842            SqlValue::Integer(self.offset as i64),
843        ]
844    }
845
846    #[cfg(feature = "sql")]
847    /// Get the total number of SQL parameter values (parameters + pagination)
848    /// This counts only non-empty values, matching the behavior of to_values()
849    pub fn total_parameters(&self) -> usize {
850        let parameter_count: usize = self
851            .parameters
852            .inner()
853            .values()
854            .map(|param| {
855                param
856                    .values()
857                    .iter()
858                    .filter(|v| !v.trim().is_empty())
859                    .count()
860            })
861            .sum();
862
863        parameter_count + 2 // +2 for limit and offset
864    }
865}