safety_postgres/access/
conditions.rs

1use crate::access::conditions::IsInJoinedTable::Yes;
2use crate::access::errors::{ConditionError, ConditionErrorGenerator};
3use crate::access::validators::validate_string;
4
5/// Provides the available comparison operators for standardizing input for the `Conditions.add_condition()` method.
6///
7/// The available comparison operators are:
8///  - `Equal`: Represents the equality condition, where the column and the value are chained by "="
9///  - `Lower`: Represents the less than condition, where the column and the value are chained by "<"
10///  - `Greater`: Represents the greater than condition, where the column and the value are chained by ">"
11///  - `LowerEq`: Represents the less than or equal to condition, where the column and the value are chained by "<="
12///  - `GreaterEq`: Represents the greater than or equal to condition, where the column and the value are chained by ">="
13#[derive(Clone)]
14pub enum ComparisonOperator {
15    Equal,
16    Lower,
17    Grater,
18    LowerEq,
19    GraterEq,
20}
21
22/// Represents whether the column is from a joined table or not.
23///
24/// The available variants are:
25///  - `Yes`: Represents that the column is from a joined tables.
26///    It contains the following fields:
27///      - `schema_name`: The name of the schema of the joined table which has the condition column (if applicable).
28///      - `table_name`: The name of the joined table which has the condition column.
29///  - `No`: Represents that the column is not from a joined table.
30#[derive(Clone)]
31pub enum IsInJoinedTable {
32    Yes {
33        schema_name: String,
34        table_name: String,
35    },
36    No,
37}
38
39/// Provides the available logical operators for combining conditions between a previous condition.
40///
41/// The available logical operators are:
42///  - `FirstCondition`: Represents the first condition, used to start the condition chain.
43///  - `And`: Represents the logical "AND" operator, which combines multiple conditions with a logical AND.
44///  - `Or`: Represents the logical "OR" operator, which combines multiple conditions with a logical OR.
45#[derive(Clone, PartialEq, Debug)]
46pub enum LogicalOperator {
47    FirstCondition,
48    And,
49    Or,
50}
51
52/// Represents a condition to be used in an execution.
53///
54/// # Fields
55/// - `is_joined_table_condition`: A flag indicating whether the condition belongs to a joined table or the main table.
56/// - `key`: The column name to apply the condition on.
57/// - `operator`: The comparison operator to use for the condition.
58/// - `value`: The value to compare against.
59#[derive(Clone)]
60struct Condition {
61    is_joined_table_condition: IsInJoinedTable,
62    key: String,
63    operator: ComparisonOperator,
64    value: String,
65}
66
67/// Represents a set of conditions to be used in an execution.
68///
69/// # Example
70/// ```rust
71/// use safety_postgres::access::conditions::ComparisonOperator::{Equal, Lower};
72/// use safety_postgres::access::conditions::Conditions;
73/// use safety_postgres::access::conditions::IsInJoinedTable::{No, Yes};
74/// use safety_postgres::access::conditions::LogicalOperator::{And, FirstCondition};
75///
76/// let mut conditions = Conditions::new();
77///
78/// conditions.add_condition("column1", "condition1", Equal, FirstCondition, No).expect("add condition failed");
79/// conditions.add_condition("column2", "condition2", Lower, And, Yes{
80///     schema_name: "schema_name".to_string(), table_name: "table_name".to_string()
81/// }).expect("add condition failed");
82///
83/// assert_eq!(
84///     conditions.get_condition_text(),
85///     "column1 = condition1 AND schema_name.table_name.column2 < condition2")
86///
87/// ```
88/// And you can specify the condition more intuitive using
89/// `Conditions.add_condition_from_str(column, value, condition_operator, condition_chain_operator, is_joined_table_condition)` method.
90///
91/// ```rust
92/// use safety_postgres::access::conditions::{Conditions, IsInJoinedTable};
93///
94/// let mut conditions = Conditions::new();
95///
96/// conditions.add_condition_from_str(
97///     "column1",
98///     "condition1",
99///     "eq",
100///     "", IsInJoinedTable::No).expect("add failed");
101/// conditions.add_condition_from_str(
102///     "column2",
103///     "condition2",
104///     ">=",
105///     "or",
106///     IsInJoinedTable::Yes{
107///         schema_name: "schema_name".to_string(), table_name: "table_name".to_string()
108///     }).expect("add failed");
109///
110/// assert_eq!(
111///     conditions.get_condition_text(),
112///     "column1 = condition1 OR schema_name.table_name.column2 >= condition2")
113/// ```
114#[derive(Clone)]
115pub struct Conditions {
116    logics: Vec<LogicalOperator>,
117    conditions: Vec<Condition>,
118}
119
120impl Conditions {
121    /// Creates a new empty `Conditions` instance.
122    pub fn new() -> Self {
123        Self {
124            logics: Vec::new(),
125            conditions: Vec::new(),
126        }
127    }
128
129    /// Adds a condition based on the input string parameters.
130    ///
131    /// # Arguments
132    ///
133    /// * `column` - The name of the column to compare.
134    /// * `value` - The value to compare against.
135    /// * `comparison_operator` - The operator to use for the comparison.
136    ///   * Available operator:
137    ///     * Equal: "=", "equal", "eq"
138    ///     * Greater: ">", "greater", "gt"
139    ///     * GreaterEqual: ">=", "greater_equal", "ge", "greater_eq"
140    ///     * Lower: "<", "lower", "lt"
141    ///     * LowerEqual: "<=", "lower_equal", "le", "lower_eq"
142    /// * `condition_chain_operator` - The operator to use for chaining multiple conditions.
143    ///   * Available operator:
144    ///     * FirstCondition(there is no previous condition): "", "first", "none"
145    ///     * And: "and", "&"
146    ///     * Or: "or", "|"
147    /// * `is_joined_table_condition` - Whether the condition is for a joined table.
148    ///
149    /// # Errors
150    ///
151    /// Returns a `ConditionError` if there's an error in the input parameters.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use safety_postgres::access::conditions::Conditions;
157    /// use safety_postgres::access::conditions::IsInJoinedTable::No;
158    ///
159    /// let mut conditions = Conditions::new();
160    /// conditions
161    ///     .add_condition_from_str("name", "John", "=", "first", No)
162    ///     .expect("adding condition failed");
163    /// conditions
164    ///     .add_condition_from_str("age", "40", "le", "or", No)
165    ///     .expect("adding condition failed");
166    ///
167    /// assert_eq!(conditions.get_condition_text(), "name = John OR age <= 40")
168    /// ```
169    pub fn add_condition_from_str(&mut self, column: &str, value: &str, comparison_operator: &str, condition_chain_operator: &str, is_joined_table_condition: IsInJoinedTable) -> Result<&mut Self, ConditionError> {
170        let comparison_op = match comparison_operator {
171            "=" | "equal" | "eq" => ComparisonOperator::Equal,
172            ">" | "greater" | "gt" => ComparisonOperator::Grater,
173            ">=" | "greater_equal" | "ge" | "greater_eq" => ComparisonOperator::GraterEq,
174            "<" | "lower" | "lt" => ComparisonOperator::Lower,
175            "<=" | "lower_equal" | "le" | "lower_eq" => ComparisonOperator::LowerEq,
176            _ => return Err(ConditionError::InputInvalidError(format!("'comparison operator' can select symbol('=', '>', '<', '>=', '<=') or some specify string, but got {}", comparison_operator))),
177        };
178        let condition_chain_op = match condition_chain_operator {
179            "" | "first" | "none" => LogicalOperator::FirstCondition,
180            "and" | "&" => LogicalOperator::And,
181            "or" | "|" => LogicalOperator::Or,
182            _ => return Err(ConditionError::InputInvalidError(format!("'condition_chain_operator' indicates the chain operator between the previous condition and the current condition by symbols('&', '|') or specified strings, but got {}", condition_chain_operator))),
183        };
184
185        self.add_condition(column, value, comparison_op, condition_chain_op, is_joined_table_condition)
186    }
187
188    /// Adds a condition to the query builder.
189    ///
190    /// # Arguments
191    ///
192    /// * `column` - The column name to which the condition is applied.
193    /// * `value` - The value for comparison.
194    /// * `comparison` - The operator used for comparison.
195    /// * `condition_chain` - The logical operator used to chain the conditions.
196    /// * `is_joined_table_condition` - Indicates whether the condition is for a joined table or not.
197    ///
198    /// # Returns
199    ///
200    /// A mutable reference to `Self (Conditions)` if the condition is successfully added, otherwise a `ConditionError`.
201    ///
202    /// # Examples
203    ///
204    /// ```
205    /// use safety_postgres::access::conditions::Conditions;
206    /// use safety_postgres::access::conditions::{ComparisonOperator, LogicalOperator, IsInJoinedTable};
207    ///
208    /// let mut conditions = Conditions::new();
209    ///
210    /// let _ = conditions.add_condition(
211    ///     "name",
212    ///     "John",
213    ///     ComparisonOperator::Equal,
214    ///     LogicalOperator::FirstCondition,
215    ///     IsInJoinedTable::No).expect("add condition failed")
216    ///     .add_condition(
217    ///     "age",
218    ///     "40",
219    ///     ComparisonOperator::LowerEq,
220    ///     LogicalOperator::Or,
221    ///     IsInJoinedTable::No).expect("add condition failed");
222    ///
223    /// assert_eq!(conditions.get_condition_text(), "name = John OR age <= 40");
224    /// ```
225    pub fn add_condition(&mut self, column: &str, value: &str, comparison: ComparisonOperator, condition_chain: LogicalOperator, is_joined_table_condition: IsInJoinedTable) -> Result<&mut Self, ConditionError> {
226        validate_string(column, "column", &ConditionErrorGenerator)?;
227
228        let mut validated_condition_chain: LogicalOperator = condition_chain.clone();
229        if let LogicalOperator::FirstCondition = condition_chain  {
230            if !self.conditions.is_empty() {
231                return Err(ConditionError::InputInvalidError(
232                    "Already condition exists. 'FirstCondition' can be used only specifying the first condition.".to_string()));
233            }
234        }
235        else {
236            if self.conditions.is_empty() {
237                eprintln!("The first condition should have 'FirstCondition' as 'condition_chain'. Automatically converted.");
238                validated_condition_chain = LogicalOperator::FirstCondition;
239            }
240        }
241
242        match &is_joined_table_condition {
243            Yes {schema_name, table_name} => {
244                if !schema_name.is_empty() && table_name.is_empty() {
245                    return Err(
246                        ConditionError::InputInvalidError(
247                            "`table_name` must be specified when `schema_name` name is specified".to_string()
248                        ))
249                }
250            },
251            IsInJoinedTable::No => {}
252        }
253
254        let condition = Condition {
255            is_joined_table_condition,
256            key: column.to_string(),
257            operator: comparison,
258            value: value.to_string(),
259        };
260
261        self.logics.push(validated_condition_chain);
262        self.conditions.push(condition);
263
264        Ok(self)
265    }
266
267    /// Checks if the conditions is empty.
268    ///
269    /// # Returns
270    ///
271    /// Returns `true` if the conditions is empty, `false` otherwise.
272    ///
273    pub(super) fn is_empty(&self) -> bool {
274        self.conditions.is_empty()
275    }
276
277    /// Generates the SQL statement text for the conditions.
278    ///
279    /// # Arguments
280    ///
281    /// * `start_index` - The starting index of the statement parameters' placeholder.
282    ///
283    /// # Returns
284    ///
285    /// The generated SQL statement text.
286    ///
287    /// # Example
288    ///
289    /// ```
290    /// use safety_postgres::access::conditions::{Conditions, IsInJoinedTable};
291    ///
292    /// let mut conditions = Conditions::new();
293    /// conditions.add_condition_from_str("name", "John", "=", "first", IsInJoinedTable::No).expect("add condition failed");
294    /// conditions.add_condition_from_str("age", "40", "le", "or", IsInJoinedTable::No).expect("add condition failed");
295    ///
296    /// let statement_text = conditions.get_condition_text();
297    ///
298    /// assert_eq!(conditions.get_condition_text(), "name = John OR age <= 40");
299    /// ```
300    pub(super) fn generate_statement_text(&self, start_index: usize) -> String {
301        let mut statement_texts: Vec<String> = Vec::new();
302
303        for (index, (condition, logic)) in self.conditions.iter().zip(&self.logics).enumerate() {
304            if statement_texts.is_empty() {
305                statement_texts.push("WHERE".to_string());
306            }
307            match logic {
308                LogicalOperator::FirstCondition => {},
309                LogicalOperator::And => statement_texts.push("AND".to_string()),
310                LogicalOperator::Or => statement_texts.push("OR".to_string()),
311            }
312            let condition_text = condition.generate_statement_text();
313            let statement_text = format!("{} ${}", condition_text, index + start_index + 1);
314            statement_texts.push(statement_text);
315        }
316
317        statement_texts.join(" ")
318    }
319
320    /// Returns the condition text generated by the conditions you set.
321    ///
322    ///
323    /// # Returns
324    ///
325    /// The set condition as a `String`.
326    pub fn get_condition_text(&self) -> String {
327        let mut conditions_txt: Vec<String> = Vec::new();
328
329        for (condition, logic) in self.conditions.iter().zip(&self.logics) {
330            match logic {
331                LogicalOperator::FirstCondition => {},
332                LogicalOperator::And => conditions_txt.push("AND".to_string()),
333                LogicalOperator::Or => conditions_txt.push("OR".to_string()),
334            }
335            let condition_txt = format!("{} {}", condition.generate_statement_text(), condition.value);
336            conditions_txt.push(condition_txt);
337        }
338
339        conditions_txt.join(" ")
340    }
341
342    /// Retrieves the values of the conditions as flatten vec.
343    pub(super) fn get_flat_values(&self) -> Vec<String> {
344        self.conditions.iter().map(|condition| condition.value.clone()).collect::<Vec<String>>()
345    }
346}
347
348impl Condition {
349    /// Generates one part of the condition by the set condition.
350    fn generate_statement_text(&self) -> String {
351        let table_name = match &self.is_joined_table_condition {
352            Yes{ schema_name, table_name } => {
353                if schema_name.is_empty() & table_name.is_empty() {
354                    self.key.to_string()
355                }
356                else if schema_name.is_empty() {
357                    format!("{}.{}", table_name, self.key)
358                }
359                else {
360                    format!("{}.{}.{}", schema_name, table_name, self.key)
361                }
362            },
363            IsInJoinedTable::No => self.key.to_string(),
364        };
365        let operator = match self.operator {
366            ComparisonOperator::Equal => "=",
367            ComparisonOperator::Lower => "<",
368            ComparisonOperator::LowerEq => "<=",
369            ComparisonOperator::Grater => ">",
370            ComparisonOperator::GraterEq => ">="
371        };
372
373        format!("{} {}", table_name, operator)
374    }
375}
376
377
378#[cfg(test)]
379mod tests {
380    use crate::access::errors::ConditionError;
381    use super::{Conditions, ComparisonOperator, LogicalOperator, IsInJoinedTable};
382
383    /// Tests that conditions can be added properly and results in the correct condition text and statement.
384    #[test]
385    fn test_add_and_get_condition() {
386        let mut conditions = Conditions::new();
387        conditions.add_condition(
388            "column1",
389            "value1",
390            ComparisonOperator::Equal,
391            LogicalOperator::FirstCondition,
392            IsInJoinedTable::No).unwrap();
393        conditions.add_condition(
394            "column2",
395            "value2",
396            ComparisonOperator::Grater,
397            LogicalOperator::And,
398            IsInJoinedTable::No).unwrap();
399        conditions.add_condition(
400            "column3",
401            "value3",
402            ComparisonOperator::LowerEq,
403            LogicalOperator::Or,
404            IsInJoinedTable::Yes {
405                schema_name: "schema1".to_string(),
406                table_name: "table1".to_string()
407            }).unwrap();
408
409        let expected_statement = "WHERE column1 = $1 AND column2 > $2 OR schema1.table1.column3 <= $3";
410        let expected_text = "column1 = value1 AND column2 > value2 OR schema1.table1.column3 <= value3";
411
412        assert_eq!(conditions.get_condition_text(), expected_text);
413        assert_eq!(conditions.generate_statement_text(0), expected_statement);
414
415        let expected_values = vec!["value1".to_string(), "value2".to_string(), "value3".to_string()];
416
417        assert_eq!(conditions.get_flat_values(), expected_values);
418    }
419
420    /// Tests adding and getting conditions using string representation of operators and confirms
421    /// it results in the correct condition text and statement.
422    #[test]
423    fn test_add_get_condition_by_str() {
424        let mut conditions = Conditions::new();
425        let _ =
426            conditions.
427                add_condition_from_str(
428                    "column1",
429                    "value1",
430                    "equal",
431                    "",
432                    IsInJoinedTable::No).unwrap()
433                .add_condition_from_str(
434                    "column2",
435                    "value2",
436                    ">=",
437                    "or",
438                    IsInJoinedTable::No).unwrap();
439
440        let expected_statement = "WHERE column1 = $1 OR column2 >= $2";
441        let expected_text = "column1 = value1 OR column2 >= value2";
442
443        assert_eq!(conditions.get_condition_text().as_str(), expected_text);
444        assert_eq!(conditions.generate_statement_text(0).as_str(), expected_statement);
445
446        let expected_values = vec!["value1", "value2"];
447
448        assert_eq!(conditions.get_flat_values(), expected_values);
449    }
450
451    /// Tests providing invalid string as comparison operator results in an appropriate error.
452    #[test]
453    fn test_invalid_comparison_str_input() {
454        let mut conditions = Conditions::new();
455        let Err(e) = conditions.add_condition_from_str(
456            "column1",
457            "value1",
458            "+",
459            "",
460            IsInJoinedTable::No,
461        ) else { panic!() };
462
463        assert_eq!(e, ConditionError::InputInvalidError(format!(
464            "'comparison operator' can select symbol('=', '>', '<', '>=', '<=') or some specify string, but got {}",
465            "+")));
466    }
467
468    /// Tests providing invalid string as condition chain operator results in an appropriate error.
469    #[test]
470    fn test_invalid_chain_str_input() {
471        let mut conditions = Conditions::new();
472        let Err(e) = conditions.add_condition_from_str(
473            "column1",
474            "value1",
475            "=",
476            "test",
477            IsInJoinedTable::No,
478        ) else { panic!() };
479
480        assert_eq!(e, ConditionError::InputInvalidError(format!(
481            "'condition_chain_operator' indicates the chain operator between the previous condition and the current condition by symbols('&', '|') or specified strings, but got {}",
482            "test")));
483
484    }
485
486    /// Tests providing invalid column name results in an appropriate error.
487    #[test]
488    fn test_invalid_column() {
489        let mut conditions = Conditions::new();
490        let Err(e) = conditions.add_condition(
491            "column1;",
492            "value1",
493            ComparisonOperator::Equal,
494            LogicalOperator::FirstCondition,
495            IsInJoinedTable::No) else { panic!() };
496        assert_eq!(e,
497                   ConditionError::InputInvalidError(
498                       format!(
499                           "'{}' has invalid characters. '{}' allows alphabets, numbers and under bar only.",
500                           "column1;", "column")
501                   ));
502    }
503
504    /// Tests that the default logical operator for the first condition is "FirstCondition".
505    #[test]
506    fn test_default_value() {
507        let mut conditions = Conditions::new();
508        conditions.add_condition(
509            "column1",
510            "value1",
511            ComparisonOperator::Equal,
512            LogicalOperator::And,
513            IsInJoinedTable::No).unwrap();
514
515        assert_eq!(conditions.logics[0], LogicalOperator::FirstCondition);
516    }
517
518    /// Tests that applying "FirstCondition" more than once results in an appropriate error.
519    #[test]
520    fn test_multiple_declaration_first_condition() {
521        let mut conditions = Conditions::new();
522        conditions.add_condition(
523            "column1",
524            "value1",
525            ComparisonOperator::Equal,
526            LogicalOperator::FirstCondition,
527            IsInJoinedTable::No
528        ).unwrap();
529        let Err(e) = conditions.add_condition(
530            "column2",
531            "value2",
532            ComparisonOperator::Equal,
533            LogicalOperator::FirstCondition,
534            IsInJoinedTable::No
535        ) else { panic!() };
536
537        assert_eq!(e,
538                   ConditionError::InputInvalidError(
539                       "Already condition exists. 'FirstCondition' can be used only specifying the first condition."
540                           .to_string()
541                   ));
542    }
543}