Skip to main content

rullst_orm/schema/
join.rs

1use super::validation::{ALLOWED_OPERATORS, validate_identifier, validate_table_name};
2
3pub struct JoinClause {
4    pub table: String,
5    pub conditions: Vec<String>,
6    pub bindings: Vec<crate::RullstValue>,
7    pub errors: Vec<crate::Error>,
8}
9
10impl JoinClause {
11    pub fn new(table: impl Into<String>) -> Self {
12        let table = table.into();
13        let mut clause = Self {
14            table,
15            conditions: vec![],
16            bindings: vec![],
17            errors: vec![],
18        };
19        if let Err(error) = validate_table_name(&clause.table) {
20            clause.errors.push(error);
21        }
22        clause
23    }
24
25    /// Adds a column-to-column JOIN condition.
26    ///
27    /// This prevents SQL injection — column names should always be hardcoded, never
28    /// derived from user input. Returns errors internally rather than panicking.
29    pub fn on(&mut self, first: &str, operator: &str, second: &str) -> &mut Self {
30        if let Err(e) = validate_identifier(first) {
31            self.errors.push(crate::Error::Validation(format!(
32                "JoinClause::on — invalid identifier for `first`: {:?}",
33                e
34            )));
35        }
36        if let Err(e) = validate_identifier(second) {
37            self.errors.push(crate::Error::Validation(format!(
38                "JoinClause::on — invalid identifier for `second`: {:?}",
39                e
40            )));
41        }
42        if !ALLOWED_OPERATORS.contains(&operator) {
43            self.errors.push(crate::Error::Validation(format!(
44                "JoinClause::on — invalid operator '{}'. Allowed: {:?}",
45                operator, ALLOWED_OPERATORS
46            )));
47        }
48        self.conditions
49            .push(format!("{} {} {}", first, operator, second));
50        self
51    }
52
53    pub fn on_eq<T: Into<crate::RullstValue>>(&mut self, column: &str, value: T) -> &mut Self {
54        if let Err(e) = validate_identifier(column) {
55            self.errors.push(crate::Error::Validation(format!(
56                "JoinClause::on_eq — invalid identifier for `column`: {:?}",
57                e
58            )));
59        }
60        self.conditions.push(format!("{} = ?", column));
61        self.bindings.push(value.into());
62        self
63    }
64
65    pub fn to_sql(&self) -> String {
66        self.conditions.join(" AND ")
67    }
68}