Repository: njord-rs/njord (language: rust)

Issue:
Add UNION



Discussion / hints:

Fix patch:
diff --git a/njord/db/insert.db b/njord/db/insert.db
index d4d2189a..24ace16d 100644
Binary files a/njord/db/insert.db and b/njord/db/insert.db differ
diff --git a/njord/src/keys.rs b/njord/src/keys.rs
index 5be6b4d4..1ae42cd5 100644
--- a/njord/src/keys.rs
+++ b/njord/src/keys.rs
@@ -83,8 +83,8 @@ where
     where
         D: Deserializer<'de>,
     {
-        let value = T::from_str(&String::deserialize(deserializer)?)
-            .map_err(serde::de::Error::custom)?;
+        let value =
+            T::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)?;
         Ok(PrimaryKey(value))
     }
 }
@@ -137,4 +137,10 @@ impl<'de, T: Deserialize<'de>> Deserialize<'de> for AutoIncrementPrimaryKey<T> {
         let value: Option<T> = Option::deserialize(deserializer)?;
         Ok(AutoIncrementPrimaryKey(value))
     }
-}
\ No newline at end of file
+}
+
+impl<T: PartialEq> PartialEq for AutoIncrementPrimaryKey<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.0 == other.0
+    }
+}
diff --git a/njord/src/sqlite/select.rs b/njord/src/sqlite/select.rs
index 08b099b7..514e72bf 100644
--- a/njord/src/sqlite/select.rs
+++ b/njord/src/sqlite/select.rs
@@ -31,7 +31,7 @@ use crate::{
     condition::Condition,
     sqlite::util::{
         generate_group_by_str, generate_having_str, generate_limit_str, generate_offset_str,
-        generate_order_by_str, generate_where_condition_str, remove_quotes_and_backslashes,
+        generate_order_by_str, generate_where_condition_str,
     },
 };
 use std::collections::HashMap;
@@ -53,7 +53,10 @@ use crate::table::Table;
 /// # Returns
 ///
 /// A `SelectQueryBuilder` instance.
-pub fn select<T: Table + Default>(conn: &Connection, columns: Vec<String>) -> SelectQueryBuilder<T> {
+pub fn select<T: Table + Default>(
+    conn: &Connection,
+    columns: Vec<String>,
+) -> SelectQueryBuilder<T> {
     SelectQueryBuilder::new(conn, columns)
 }
 
@@ -70,6 +73,7 @@ pub struct SelectQueryBuilder<'a, T: Table + Default> {
     offset: Option<usize>,
     having_condition: Option<Condition>,
     except_clauses: Option<Vec<SelectQueryBuilder<'a, T>>>,
+    union_clauses: Option<Vec<SelectQueryBuilder<'a, T>>>,
 }
 
 impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
@@ -92,6 +96,7 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
             offset: None,
             having_condition: None,
             except_clauses: None,
+            union_clauses: None,
         }
     }
 
@@ -204,6 +209,29 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
         self
     }
 
+    /// Adds a UNION clause to the query, allowing you to combine results from another query.
+    ///
+    /// This method modifies the current query builder to include the results of the specified
+    /// `other_query`. If there are already existing UNION clauses, the new clause will be added
+    /// to the list. If no UNION clauses exist, a new list will be created with the provided
+    /// query.
+    ///
+    /// # Arguments
+    ///
+    /// * `other_query` - A `SelectQueryBuilder` instance that represents the query whose results
+    ///   should be combined with the current query.
+    ///
+    /// # Returns
+    ///
+    /// Returns the modified `SelectQueryBuilder` instance with the new UNION clause added.
+    pub fn union(mut self, other_query: SelectQueryBuilder<'a, T>) -> Self {
+        match self.union_clauses {
+            Some(ref mut clauses) => clauses.push(other_query),
+            None => self.union_clauses = Some(vec![other_query]),
+        }
+        self
+    }
+
     /// Builds the query string, this function should be used internally.
     fn build_query(&self) -> String {
         let columns_str = self.columns.join(", ");
@@ -219,7 +247,8 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
         let order_by_str = generate_order_by_str(&self.order_by);
         let limit_str = generate_limit_str(self.limit);
         let offset_str = generate_offset_str(self.offset);
-        let having_str = generate_having_str(self.group_by.is_some(), self.having_condition.as_ref());
+        let having_str =
+            generate_having_str(self.group_by.is_some(), self.having_condition.as_ref());
 
         let mut query = format!(
             "SELECT {}{} FROM {} {} {} {} {} {}",
@@ -241,6 +270,14 @@ impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
             }
         }
 
+        // Handle UNION clauses
+        if let Some(union_clauses) = &self.union_clauses {
+            for union_query in union_clauses {
+                let union_sql = union_query.build_query();
+                query = format!("{} UNION {}", query, union_sql);
+            }
+        }
+
         query
     }
 
diff --git a/njord_derive/src/lib.rs b/njord_derive/src/lib.rs
index 693c9114..6e576cf7 100644
--- a/njord_derive/src/lib.rs
+++ b/njord_derive/src/lib.rs
@@ -86,7 +86,7 @@ pub fn table_derive(input: TokenStream) -> TokenStream {
                     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                         write!(f, "{}", self.get_name())?;
                         for (name, value) in self.get_column_fields().iter().zip(self.get_column_values()) {
-                            write!(f, ", {}: {}", name, value);
+                            write!(f, ", {}: {}", name, value)?;
                         }
                         Ok(())
                     }


Test patch:
diff --git a/njord/tests/sqlite_test.rs b/njord/tests/sqlite_test.rs
index 71cc6a84..124cb54f 100644
--- a/njord/tests/sqlite_test.rs
+++ b/njord/tests/sqlite_test.rs
@@ -1,12 +1,12 @@
 // integrations tests for sqlite
 
 use njord::condition::Condition;
+use njord::keys::{AutoIncrementPrimaryKey, PrimaryKey};
 use njord::sqlite::{self};
 use njord::table::Table;
 use njord_derive::Table;
 use std::collections::HashMap;
 use std::path::Path;
-use njord::keys::{AutoIncrementPrimaryKey, PrimaryKey};
 
 #[derive(Table)]
 #[table_name = "users"]
@@ -406,7 +406,7 @@ fn select_except() {
         "id".to_string(),
         "username".to_string(),
         "email".to_string(),
-        "address".to_string()
+        "address".to_string(),
     ];
 
     let condition1 = Condition::Eq("username".to_string(), "mjovanc".to_string());
@@ -429,10 +429,7 @@ fn select_except() {
                 .where_clause(condition3);
 
             // Test a chain of EXCEPT queries (query1 EXCEPT query2 EXCEPT query3)
-            let result = query1
-                .except(query2)
-                .except(query3)
-                .build();
+            let result = query1.except(query2).except(query3).build();
 
             match result {
                 Ok(r) => {
@@ -443,4 +440,56 @@ fn select_except() {
         }
         Err(e) => panic!("Failed to SELECT: {:?}", e),
     };
-}
\ No newline at end of file
+}
+
+#[test]
+fn select_union() {
+    let db_relative_path = "./db/select.db";
+    let db_path = Path::new(&db_relative_path);
+    let conn = sqlite::open(db_path);
+
+    let columns = vec![
+        "id".to_string(),
+        "username".to_string(),
+        "email".to_string(),
+        "address".to_string(),
+    ];
+
+    let condition1 = Condition::Eq("id".to_string(), 42.to_string());
+    let condition2 = Condition::Eq("id".to_string(), 43.to_string());
+
+    match conn {
+        Ok(c) => {
+            // Create a new connection for each query builder
+            let query1 = sqlite::select(&c, columns.clone())
+                .from(User::default())
+                .where_clause(condition1);
+
+            let query2 = sqlite::select(&c, columns.clone())
+                .from(User::default())
+                .where_clause(condition2);
+
+            // Test a chain of UNION queries (query1 UNION query2)
+            let result = query1.union(query2).build();
+
+            match result {
+                Ok(r) => {
+                    // We expect two results: mjovanc and otheruser
+                    assert_eq!(r.len(), 2, "Expected 2 results from the UNION query.");
+                    assert_eq!(
+                        r[0].id,
+                        AutoIncrementPrimaryKey::new(Some(42)),
+                        "First user should be mjovanc."
+                    );
+                    assert_eq!(
+                        r[1].id,
+                        AutoIncrementPrimaryKey::new(Some(43)),
+                        "Second user should be otheruser."
+                    );
+                }
+                Err(e) => panic!("Failed to SELECT with UNION: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    }
+}