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}