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

Issue:
Add Oracle DBMS support



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml
index f9c8b939..63b41150 100644
--- a/.github/workflows/core.yml
+++ b/.github/workflows/core.yml
@@ -1,4 +1,4 @@
-name: build
+name: build-mysql
 
 on:
   push:
@@ -17,8 +17,8 @@ on:
       - "**/website/**"
 
 jobs:
-  build:
-    name: build
+  mysql:
+    name: mysql
     runs-on: ubuntu-latest
 
     services:
@@ -57,13 +57,7 @@ jobs:
         run: cargo clippy --all-targets --all-features
 
       - name: Build Project
-        run: cargo build --release
-
-      - name: Running Unit Tests for SQLite
-        run: cargo test sqlite
-
-      - name: Running Integration Tests for SQLite
-        run: cargo test --test sqlite_tests
+        run: cargo build --release --features "mysql"
 
       - name: Wait for MySQL to be ready
         run: |
@@ -74,14 +68,11 @@ jobs:
 
       - name: Set up MySQL schema
         env:
-          MYSQL_PWD: rootpassword
+          MYSQL_PWD: njord_rootpwd
         run: |
           echo "Injecting schema and data into MySQL..."
           mysql -h 127.0.0.1 -u njord_user -pnjord_password njord_db < njord/db/test/mysql.sql
 
-      - name: Running Unit Tests for MySQL
-        run: cargo test mysql
-
       - name: Running Integration Tests for MySQL
         env:
           MYSQL_DATABASE: njord_db
@@ -89,3 +80,68 @@ jobs:
           MYSQL_PASSWORD: njord_password
           MYSQL_HOST: 127.0.0.1
         run: cargo test --test mysql_tests
+  oracle:
+    name: oracle
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install Rust
+        uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+
+      - name: Cache Cargo Dependencies
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo
+          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+      - name: Run Clippy
+        run: cargo clippy --all-targets --all-features
+
+      - name: Build Project
+        run: cargo build --release --features "oracle"
+
+      - run: mkdir ${{ github.workspace }}/database-files
+
+      - uses: gvenzl/setup-oracle-free@v1
+        with:
+          app-user: njord_user
+          app-user-password: njord_password
+          volume: ${{ github.workspace }}/database-files
+          startup-scripts: ${{ github.workspace }}/njord/db/test/oracle
+      # Install Oracle Instant Client
+      - name: Install Oracle Instant Client
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y libaio1
+          wget https://download.oracle.com/otn_software/linux/instantclient/2350000/instantclient-basic-linux.x64-23.5.0.24.07.zip
+          unzip instantclient-basic-linux.x64-23.5.0.24.07.zip
+          sudo mkdir -p /opt/oracle # Create the directory structure if it doesn't exist
+          sudo mv instantclient_23_5 /opt/oracle/instantclient
+          sudo sh -c "echo /opt/oracle/instantclient > /etc/ld.so.conf.d/oracle-instantclient.conf"
+          sudo ldconfig
+
+      - name: Set Oracle Library Path
+        run: |
+          echo "/opt/oracle/instantclient" | sudo tee -a /etc/ld.so.conf.d/oracle-instantclient.conf
+          sudo ldconfig
+        env:
+          LD_LIBRARY_PATH: "/opt/oracle/instantclient"
+
+      - name: Check Oracle Client
+        run: |
+          ls /opt/oracle/instantclient
+          ldd /opt/oracle/instantclient/libclntsh.so
+
+      - name: Running Integration Tests for Oracle
+        env:
+          ORACLE_DATABASE: FREEPDB1
+          APP_USER: test
+          APP_USER_PASSWORD: test
+          ORACLE_HOST: 127.0.0.1
+        run: cargo test --test oracle_tests
diff --git a/Cargo.lock b/Cargo.lock
index afbb0eb4..52798934 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -61,6 +61,21 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
 
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "arrayvec"
 version = "0.7.6"
@@ -275,6 +290,18 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 
+[[package]]
+name = "chrono"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "clang-sys"
 version = "1.8.1"
@@ -395,14 +422,38 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "darling"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
+dependencies = [
+ "darling_core 0.13.4",
+ "darling_macro 0.13.4",
+]
+
 [[package]]
 name = "darling"
 version = "0.20.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
 dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.20.10",
+ "darling_macro 0.20.10",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.10.0",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -415,17 +466,28 @@ dependencies = [
  "ident_case",
  "proc-macro2",
  "quote",
- "strsim",
+ "strsim 0.11.1",
  "syn 2.0.82",
 ]
 
+[[package]]
+name = "darling_macro"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
+dependencies = [
+ "darling_core 0.13.4",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "darling_macro"
 version = "0.20.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
 dependencies = [
- "darling_core",
+ "darling_core 0.20.10",
  "quote",
  "syn 2.0.82",
 ]
@@ -867,6 +929,29 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "iana-time-zone"
+version = "0.1.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
 [[package]]
 name = "ident_case"
 version = "1.0.1"
@@ -1110,7 +1195,7 @@ version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "afe0450cc9344afff34915f8328600ab5ae19260802a334d0f72d2d5bdda3bfe"
 dependencies = [
- "darling",
+ "darling 0.20.10",
  "heck",
  "num-bigint",
  "proc-macro-crate",
@@ -1195,6 +1280,7 @@ dependencies = [
  "log",
  "mysql 25.0.1",
  "njord_derive",
+ "oracle 0.6.2",
  "rand",
  "rusqlite",
  "serde",
@@ -1313,6 +1399,44 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "oracle"
+version = "0.1.0"
+dependencies = [
+ "njord",
+ "njord_derive",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "oracle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e3c69cf2fb1e7215b88ac1c9dba882f366bfe900f8bbf03ed524999f35172b3"
+dependencies = [
+ "cc",
+ "chrono",
+ "once_cell",
+ "oracle_procmacro",
+ "paste",
+ "rustversion",
+]
+
+[[package]]
+name = "oracle_procmacro"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad247f3421d57de56a0d0408d3249d4b1048a522be2013656d92f022c3d8af27"
+dependencies = [
+ "darling 0.13.4",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -1336,6 +1460,12 @@ dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
 [[package]]
 name = "peeking_take_while"
 version = "0.1.2"
@@ -1744,6 +1874,12 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
+
 [[package]]
 name = "ryu"
 version = "1.0.17"
@@ -1936,6 +2072,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
 [[package]]
 name = "strsim"
 version = "0.11.1"
@@ -2444,6 +2586,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-registry"
 version = "0.2.0"
diff --git a/Cargo.toml b/Cargo.toml
index d8ad94c0..1e1ba8a5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ members = [
     "njord_derive",
     "njord_examples/sqlite",
     "njord_examples/mysql",
+    "njord_examples/oracle",
 ]
 
 resolver = "2"
diff --git a/docker-compose.yml b/docker-compose.yml
index 99c46843..d5a0fe0e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: "3.8"
-
 services:
   mysql:
     image: mysql:8.0
@@ -14,7 +12,30 @@ services:
     volumes:
       - mysql_data:/var/lib/mysql
       - ./njord_examples/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
+      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
       - ./njord/db/test/mysql.sql:/docker-entrypoint-initdb.d/tests.sql
 
+  oracle:
+    image: gvenzl/oracle-free:latest
+    container_name: njord_oracle
+    environment:
+      ORACLE_PASSWORD: njord_password
+      APP_USER: test
+      APP_USER_PASSWORD: test
+    ports:
+      - "1521:1521"
+      - "5500:5500"
+    volumes:
+      - ./njord_examples/oracle/init_scripts:/container-entrypoint-initdb.d
+      - ./njord/db/test/oracle:/container-entrypoint-initdb.d
+    healthcheck:
+      test: ["CMD", "healthcheck.sh"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
+      start_period: 5s
+      start_interval: 5s
+
 volumes:
   mysql_data:
+  oracle_data:
diff --git a/njord/Cargo.toml b/njord/Cargo.toml
index 5842a62f..6646fdc0 100644
--- a/njord/Cargo.toml
+++ b/njord/Cargo.toml
@@ -19,6 +19,7 @@ log = "0.4.22"
 rand = "0.8.4"
 serde = { version = "1.0.210", features = ["derive"] }
 mysql = "25.0.1"
+oracle = { version = "0.6.2", features = ["chrono"] }
 
 [dev-dependencies]
 njord_derive = { version = "0.4.0", path = "../njord_derive" }
@@ -35,6 +36,7 @@ njord_derive = { version = "0.4.0", path = "../njord_derive" }
 default = ["sqlite"]
 sqlite = []
 mysql = []
+oracle = []
 
 [[test]]
 name = "sqlite_tests"
@@ -43,3 +45,7 @@ path = "tests/sqlite/mod.rs"
 [[test]]
 name = "mysql_tests"
 path = "tests/mysql/mod.rs"
+
+[[test]]
+name = "oracle_tests"
+path = "tests/oracle/mod.rs"
diff --git a/njord/src/lib.rs b/njord/src/lib.rs
index a3e70879..c28c6ce5 100644
--- a/njord/src/lib.rs
+++ b/njord/src/lib.rs
@@ -30,12 +30,15 @@
 pub mod column;
 pub mod condition;
 pub mod keys;
+pub mod query;
 pub mod table;
 pub mod util;
-pub mod query;
 
 #[cfg(feature = "sqlite")]
 pub mod sqlite;
 
 #[cfg(feature = "mysql")]
 pub mod mysql;
+
+#[cfg(feature = "oracle")]
+pub mod oracle;
diff --git a/njord/src/oracle/delete.rs b/njord/src/oracle/delete.rs
new file mode 100644
index 00000000..bc139df2
--- /dev/null
+++ b/njord/src/oracle/delete.rs
@@ -0,0 +1,173 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use std::collections::HashMap;
+
+use crate::{
+    condition::Condition,
+    oracle::util::{
+        generate_limit_str, generate_offset_str, generate_order_by_str,
+        generate_where_condition_str, remove_quotes_and_backslashes,
+    },
+};
+
+use log::info;
+use oracle::Connection;
+
+use crate::table::Table;
+
+/// Constructs a new DELETE query builder.
+///
+/// # Arguments
+///
+/// * `conn` - A `Connection` to the Oracle database.
+///
+/// # Returns
+///
+/// A `DeleteQueryBuilder` instance.
+pub fn delete<T: Table + Default>(conn: &mut Connection) -> DeleteQueryBuilder<T> {
+    DeleteQueryBuilder::new(conn)
+}
+
+/// A builder for constructing DELETE queries.
+pub struct DeleteQueryBuilder<'a, T: Table + Default> {
+    conn: &'a mut Connection,
+    table: Option<T>,
+    where_condition: Option<Condition<'a>>,
+    order_by: Option<HashMap<Vec<String>, String>>,
+    limit: Option<usize>,
+    offset: Option<usize>,
+}
+
+impl<'a, T: Table + Default> DeleteQueryBuilder<'a, T> {
+    /// Creates a new `DeleteQueryBuilder` instance.
+    ///
+    /// # Arguments
+    ///
+    /// * `conn` - A `Connection` to the Oracle database.
+    pub fn new(conn: &'a mut Connection) -> Self {
+        DeleteQueryBuilder {
+            conn,
+            table: None,
+            where_condition: None,
+            order_by: None,
+            limit: None,
+            offset: None,
+        }
+    }
+
+    /// Sets the table from which to delete data.
+    ///
+    /// # Arguments
+    ///
+    /// * `table` - An instance of the table from which to delete data.
+    pub fn from(mut self, table: T) -> Self {
+        self.table = Some(table);
+        self
+    }
+
+    /// Sets the WHERE clause condition.
+    ///
+    /// # Arguments
+    ///
+    /// * `condition` - The condition to be applied in the WHERE clause.
+    pub fn where_clause(mut self, condition: Condition<'a>) -> Self {
+        self.where_condition = Some(condition);
+        self
+    }
+
+    /// Sets the ORDER BY clause columns and order direction.
+    ///
+    /// # Arguments
+    ///
+    /// * `col_and_order` - A hashmap representing the columns and their order directions.
+    pub fn order_by(mut self, col_and_order: HashMap<Vec<String>, String>) -> Self {
+        self.order_by = Some(col_and_order);
+        self
+    }
+
+    /// Sets the LIMIT clause for the query.
+    ///
+    /// # Arguments
+    ///
+    /// * `count` - The maximum number of rows to be deleted.
+    pub fn limit(mut self, count: usize) -> Self {
+        self.limit = Some(count);
+        self
+    }
+
+    /// Sets the OFFSET clause for the query.
+    ///
+    /// # Arguments
+    ///
+    /// * `offset` - The offset from which to start deleting rows.
+    pub fn offset(mut self, offset: usize) -> Self {
+        self.offset = Some(offset);
+        self
+    }
+
+    /// Builds and executes the DELETE query.
+    ///
+    /// # Returns
+    ///
+    /// A `Result` indicating success or failure of the deletion operation.
+    pub fn build(self) -> Result<(), String> {
+        let table_name = self
+            .table
+            .as_ref()
+            .map(|t| t.get_name().to_string())
+            .unwrap_or("".to_string());
+
+        // Sanitize table name from unwanted quotations or backslashes
+        let table_name_str = remove_quotes_and_backslashes(&table_name);
+        let where_condition_str = generate_where_condition_str(self.where_condition);
+        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);
+
+        // Construct the query based on defined variables above
+        let query = format!(
+            "DELETE FROM {} {} {} {}",
+            table_name_str,
+            where_condition_str,
+            order_by_str,
+            format!("{} {}", limit_str, offset_str),
+        );
+
+        info!("{}", query);
+        println!("{}", query);
+
+        // Execute SQL
+        let _ = self.conn.execute(&query, &[]);
+
+        Ok(())
+    }
+}
diff --git a/njord/src/oracle/error.rs b/njord/src/oracle/error.rs
new file mode 100644
index 00000000..e3af67f8
--- /dev/null
+++ b/njord/src/oracle/error.rs
@@ -0,0 +1,52 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use oracle::Error as OracleLibError;
+
+/// Represents errors that can occur during SQLite operations.
+#[derive(Debug)]
+pub enum OracleError {
+    /// Error that occurs during a SELECT operation.
+    SelectError(OracleLibError),
+    /// Error that occurs during an INSERT operation.
+    InsertError(OracleLibError),
+    /// Error that occurs during an UPDATE operation.
+    UpdateError(OracleLibError),
+    /// Error that occurs during a DELETE operation.
+    DeleteError(OracleLibError),
+}
+
+impl From<OracleLibError> for OracleError {
+    /// Converts a `rusqlite::Error` into a `OracleError`.
+    fn from(error: OracleLibError) -> Self {
+        OracleError::InsertError(error)
+    }
+}
diff --git a/njord/src/oracle/insert.rs b/njord/src/oracle/insert.rs
new file mode 100644
index 00000000..a0c3674f
--- /dev/null
+++ b/njord/src/oracle/insert.rs
@@ -0,0 +1,202 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024,
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use crate::{query::QueryBuilder, table::Table};
+
+use oracle::Connection;
+use rusqlite::Error as RusqliteError;
+
+use log::info;
+use std::fmt::Error;
+
+/// Inserts rows into a Oracle table.
+///
+/// This function takes a `Connection` and a vector of objects implementing
+/// the `Table` trait, which represents rows to be inserted into the table.
+/// It generates SQL INSERT statements for each row and executes them within
+/// a transaction.
+///
+/// # Arguments
+///
+/// * `conn` - A `Connection` to the Oracle database.
+/// * `table_rows` - A vector of objects implementing the `Table` trait representing
+///                  the rows to be inserted into the database.
+///
+/// # Returns
+///
+/// A `Result` containing a `String` representing the joined SQL statements
+/// if the insertion is successful, or a `RusqliteError` if an error occurs.
+pub fn insert<T: Table>(
+    conn: &mut Connection,
+    table_rows: Vec<T>,
+) -> Result<String, RusqliteError> {
+    let mut statements: Vec<String> = Vec::new();
+    for (index, table_row) in table_rows.iter().enumerate() {
+        match generate_statement(table_row, index == 0) {
+            Ok(statement) => statements.push(statement),
+            Err(_) => return Err(RusqliteError::InvalidQuery),
+        }
+    }
+
+    let joined_statements = statements.join(", ");
+
+    println!("{}", joined_statements);
+
+    let _ = match conn.execute(&joined_statements, &[]) {
+        Ok(_) => info!("Inserted into table, done."),
+        Err(err) => eprintln!("Error: {}", err),
+    };
+
+    let _ = conn.commit();
+
+    info!("Inserted into table, done.");
+
+    // FIXME: Return the number of rows affected
+    Ok(joined_statements)
+}
+
+/// Generates an SQL INSERT INTO statement for a given table row.
+///
+/// # Arguments
+///
+/// * `table_row` - A reference to an object implementing the `Table` trait.
+///
+/// # Returns
+///
+/// A `Result` containing a `String` representing the generated SQL statement
+/// if the operation is successful, or a `RusqliteError` if an error occurs.
+pub fn into<'a, T: Table + Default>(
+    conn: &'a mut Connection,
+    columns: Vec<String>,
+    subquery: Box<dyn QueryBuilder<'a> + 'a>,
+) -> Result<String, RusqliteError> {
+    let statement = generate_insert_into_statement::<T>(columns, subquery);
+    let sql = statement.unwrap();
+
+    // FIXME: Convert to transaction
+    let _ = conn.execute(&sql, &[]);
+
+    info!("Inserted into table, done.");
+
+    // FIXME: Return the number of rows affected
+    return Ok(sql);
+}
+
+/// Generates an SQL INSERT INTO statement for a given subquery.
+///
+/// # Arguments
+///
+/// * `columns` - A `Vec` of column names.
+/// * `subquery` - A `QueryBuilder` object representing the subquery.
+///
+/// # Returns
+///
+/// A `Result` containing a `String` representing the generated SQL statement
+/// if the operation is successful, or a `RusqliteError` if an error occurs.
+fn generate_insert_into_statement<'a, T: Table + Default>(
+    columns: Vec<String>,
+    subquery: Box<dyn QueryBuilder<'a> + 'a>,
+) -> Result<String, RusqliteError> {
+    let columns_str = columns.join(", ");
+    let subquery_str = subquery.to_sql();
+    let table_row = T::default();
+    let table_name = table_row.get_name().replace("\"", "").replace("\\", "");
+
+    let sql = format!(
+        "INSERT INTO {} ({}) {}",
+        table_name, columns_str, subquery_str
+    );
+
+    Ok(sql)
+}
+
+/// Generates an SQL INSERT statement for a given table row.
+///
+/// This function takes an object implementing the `Table` trait, representing
+/// a single row of data to be inserted into the database. It generates an SQL
+/// INSERT statement based on the column names and values of the table row.
+///
+/// # Arguments
+///
+/// * `table_row` - An object implementing the `Table` trait representing
+///                 a single row of data to be inserted.
+/// * `first_statement` - A boolean flag indicating whether this is the first
+///                       statement to be generated.
+///
+/// # Returns
+///
+/// A `Result` containing a `String` representing the generated SQL statement
+/// if successful, or a `Error` if an error occurs during the generation process.
+fn generate_statement<T: Table>(table_row: &T, first_statement: bool) -> Result<String, Error> {
+    // Generate strings for columns and values
+    let mut columns_str = String::new();
+    let mut values_str = String::new();
+
+    // Iterate over the fields to generate columns and values
+    let column_fields = table_row.get_column_fields();
+    let column_values = table_row.get_column_values();
+
+    for (column_name, value) in column_fields.iter().zip(column_values.iter()) {
+        // Check if the field is an AutoIncrementPrimaryKey
+        if table_row.is_auto_increment_primary_key(value) {
+            println!("Skipping AutoIncrementPrimaryKey field in SQL statement generation.");
+            continue;
+        }
+        columns_str.push_str(&format!("{}, ", column_name));
+        values_str.push_str(&format!("'{}', ", value)); // Surround values with single quotes
+    }
+
+    // Sanitize table name from unwanted quotations or backslashes
+    let table_name = table_row.get_name().replace("\"", "").replace("\\", "");
+
+    // Remove the trailing comma and space
+    if !columns_str.is_empty() {
+        columns_str.pop();
+        columns_str.pop();
+    }
+    if !values_str.is_empty() {
+        values_str.pop();
+        values_str.pop();
+    }
+
+    let sql = if first_statement {
+        format!(
+            "INSERT INTO {} ({}) VALUES ({})",
+            table_name, columns_str, values_str
+        )
+    } else {
+        format!("({})", values_str)
+    };
+
+    println!("{}", sql); // For debugging purposes
+
+    Ok(sql)
+}
diff --git a/njord/src/oracle/mod.rs b/njord/src/oracle/mod.rs
new file mode 100644
index 00000000..470fc31e
--- /dev/null
+++ b/njord/src/oracle/mod.rs
@@ -0,0 +1,75 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024,
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use oracle::{Connection, Error};
+
+pub mod delete;
+pub mod error;
+pub mod insert;
+pub mod select;
+pub mod update;
+mod util;
+
+pub use delete::delete;
+pub use error::OracleError;
+pub use insert::insert;
+pub use select::select;
+pub use update::update;
+
+/// Open a database connection.
+///
+/// This function opens a connection to a Oracle database located at the specified path.
+///
+/// # Arguments
+///
+/// * `username` - A reference to the username for the Oracle database.
+/// * `password` - A reference to the password for the Oracle database.
+/// * `connect_string` - A reference to the connect string for the Oracle database.
+///
+/// # Returns
+///
+/// Returns a `Result` containing a `PooledConn` if the operation was successful, or an `Error` if an error occurred.
+pub fn open(username: &str, password: &str, connect_string: &str) -> Result<Connection, Error> {
+    let conn = Connection::connect(username, password, connect_string);
+
+    match conn {
+        Ok(conn) => {
+            println!("Successfully connected to Oracle database");
+
+            return Ok(conn);
+        }
+        Err(err) => {
+            println!("Error: {}", err);
+
+            return Err(err);
+        }
+    }
+}
diff --git a/njord/src/oracle/select.rs b/njord/src/oracle/select.rs
new file mode 100644
index 00000000..1859d0d4
--- /dev/null
+++ b/njord/src/oracle/select.rs
@@ -0,0 +1,380 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use crate::{
+    column::Column,
+    condition::Condition,
+    oracle::util::{
+        generate_group_by_str, generate_having_str, generate_order_by_str,
+        generate_where_condition_str,
+    },
+    query::QueryBuilder,
+};
+use std::{collections::HashMap, sync::Arc};
+
+use log::info;
+use oracle::{Connection, Error};
+
+use crate::table::Table;
+use crate::util::{Join, JoinType};
+
+/// Constructs a new SELECT query builder.
+///
+/// # Arguments
+///
+/// * `conn` - A `Connection` to the Oracle database.
+/// * `columns` - A vector of strings representing the columns to be selected.
+///
+/// # Returns
+///
+/// A `SelectQueryBuilder` instance.
+pub fn select<T: Table + Default>(columns: Vec<Column>) -> SelectQueryBuilder<T> {
+    SelectQueryBuilder::new(columns)
+}
+
+/// A builder for constructing SELECT queries.
+#[derive(Clone)]
+pub struct SelectQueryBuilder<'a, T: Table + Default> {
+    table: Option<T>,
+    columns: Vec<Column<'a>>,
+    where_condition: Option<Condition<'a>>,
+    distinct: bool,
+    group_by: Option<Vec<String>>,
+    order_by: Option<HashMap<Vec<String>, String>>,
+    having_condition: Option<Condition<'a>>,
+    except_clauses: Option<Vec<SelectQueryBuilder<'a, T>>>,
+    union_clauses: Option<Vec<SelectQueryBuilder<'a, T>>>,
+    joins: Option<Vec<Join<'a>>>,
+}
+
+impl<'a, T: Table + Default> SelectQueryBuilder<'a, T> {
+    /// Creates a new `SelectQueryBuilder` instance.
+    ///
+    /// # Arguments
+    ///
+    /// * `conn` - A `Connection` to the Oracle database.
+    /// * `columns` - A vector of strings representing the columns to be selected.
+    pub fn new(columns: Vec<Column<'a>>) -> Self {
+        SelectQueryBuilder {
+            table: None,
+            columns,
+            where_condition: None,
+            distinct: false,
+            group_by: None,
+            order_by: None,
+            having_condition: None,
+            except_clauses: None,
+            union_clauses: None,
+            joins: None,
+        }
+    }
+
+    /// Sets the columns to be selected.
+    ///
+    /// # Arguments
+    ///
+    /// * `columns` - A vector of strings representing the columns to be selected.
+    pub fn select(mut self, columns: Vec<Column<'a>>) -> Self {
+        self.columns = columns;
+        self
+    }
+
+    /// Sets the DISTINCT keyword for the query.
+    pub fn distinct(mut self) -> Self {
+        self.distinct = true;
+        self
+    }
+
+    /// Sets the table from which to select data.
+    ///
+    /// # Arguments
+    ///
+    /// * `table` - The table from which to select data.
+    pub fn from(mut self, table: T) -> Self {
+        self.table = Some(table);
+        self
+    }
+
+    /// Sets the WHERE clause condition.
+    ///
+    /// # Arguments
+    ///
+    /// * `condition` - The condition to be applied in the WHERE clause.
+    pub fn where_clause(mut self, condition: Condition<'a>) -> Self {
+        self.where_condition = Some(condition);
+        self
+    }
+
+    /// Sets the GROUP BY clause columns.
+    ///
+    /// # Arguments
+    ///
+    /// * `columns` - A vector of strings representing the columns to be grouped by.
+    pub fn group_by(mut self, columns: Vec<String>) -> Self {
+        self.group_by = Some(columns);
+        self
+    }
+
+    /// Sets the ORDER BY clause columns and order direction.
+    ///
+    /// # Arguments
+    ///
+    /// * `col_and_order` - A HashMap containing column names as keys and order direction as values.
+    pub fn order_by(mut self, col_and_order: HashMap<Vec<String>, String>) -> Self {
+        self.order_by = Some(col_and_order);
+        self
+    }
+
+    /// Sets the HAVING clause condition.
+    ///
+    /// # Arguments
+    ///
+    /// * `condition` - The condition to be applied in the HAVING clause.
+    pub fn having(mut self, condition: Condition<'a>) -> Self {
+        self.having_condition = Some(condition);
+        self
+    }
+
+    /// Adds an EXCEPT clause to the query, allowing you to exclude results from another query.
+    ///
+    /// This method modifies the current query builder to exclude the results of the specified
+    /// `other_query`. If there are already existing EXCEPT clauses, the new clause will be added
+    /// to the list. If no EXCEPT 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 excluded from the current query.
+    ///
+    /// # Returns
+    ///
+    /// Returns the modified `SelectQueryBuilder` instance with the new EXCEPT clause added.
+    pub fn except(mut self, other_query: SelectQueryBuilder<'a, T>) -> Self {
+        match self.except_clauses {
+            Some(ref mut clauses) => clauses.push(other_query),
+            None => self.except_clauses = Some(vec![other_query]),
+        }
+        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
+    }
+
+    /// Adds a JOIN clause to the query, allowing you to combine rows from two or more tables based on a related column.
+    ///
+    /// This method modifies the current query builder to include a join clause with the specified join type,
+    /// target table, and condition for the join. If there are already existing JOIN clauses, the new clause
+    /// will be added to the list. If no JOIN clauses exist, a new list will be created with the provided
+    /// join information.
+    ///
+    /// # Arguments
+    ///
+    /// * `join_type` - The type of join to perform (e.g., INNER, LEFT, RIGHT, FULL).
+    /// * `table` - The table to join with the current table.
+    /// * `on_condition` - The condition that specifies how the tables are related (the ON clause).
+    ///
+    /// # Returns
+    ///
+    /// Returns the modified `SelectQueryBuilder` instance with the new JOIN clause added.
+    pub fn join(
+        mut self,
+        join_type: JoinType,
+        table: Arc<dyn Table>,
+        on_condition: Condition<'a>,
+    ) -> Self {
+        match self.joins {
+            Some(ref mut joins) => joins.push(Join::new(join_type, table, on_condition)),
+            None => self.joins = Some(vec![Join::new(join_type, table, on_condition)]),
+        }
+        self
+    }
+
+    /// Builds the query string, this function should be used internally.
+    pub fn build_query(&self) -> String {
+        let columns_str = self
+            .columns
+            .iter()
+            .map(|c| c.build())
+            .collect::<Vec<String>>()
+            .join(", ");
+
+        let table_name = self
+            .table
+            .as_ref()
+            .map(|t| t.get_name().to_string())
+            .unwrap_or("".to_string());
+
+        // Generate JOIN clauses, if any
+        let join_clauses: Vec<String> = match &self.joins {
+            Some(joins) => joins
+                .iter()
+                .map(|join| {
+                    let join_type_str = match join.join_type {
+                        JoinType::Inner => "INNER JOIN",
+                        JoinType::Left => "LEFT JOIN",
+                        JoinType::Right => "RIGHT JOIN",
+                        JoinType::Full => "FULL OUTER JOIN",
+                    };
+                    format!(
+                        "{} {} ON {}",
+                        join_type_str,
+                        join.table.get_name(),
+                        generate_where_condition_str(Some(join.on_condition.clone()))
+                            .replace("WHERE", "")
+                    )
+                })
+                .collect(),
+            None => Vec::new(),
+        };
+
+        let distinct_str = if self.distinct { "DISTINCT " } else { "" };
+        let where_condition_str = generate_where_condition_str(self.where_condition.clone());
+        let group_by_str = generate_group_by_str(&self.group_by);
+        let order_by_str = generate_order_by_str(&self.order_by);
+        let having_str =
+            generate_having_str(self.group_by.is_some(), self.having_condition.as_ref());
+
+        // Create the JOIN clause or an empty string
+        let join_clause = if !join_clauses.is_empty() {
+            join_clauses.join(" ")
+        } else {
+            String::new()
+        };
+
+        let mut query = format!(
+            "SELECT {}{} FROM {} {} {} {} {} {}",
+            distinct_str,
+            columns_str,
+            table_name,
+            join_clause,
+            where_condition_str,
+            group_by_str,
+            having_str,
+            order_by_str,
+        );
+
+        // Handle EXCEPT clauses
+        if let Some(except_clauses) = &self.except_clauses {
+            for except_query in except_clauses {
+                let except_sql = except_query.build_query();
+                query = format!("{} EXCEPT {}", query, except_sql);
+            }
+        }
+
+        // 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
+    }
+
+    /// Builds and executes the SELECT query.
+    ///
+    /// # Returns
+    ///
+    /// A `Result` containing a vector of selected table rows if successful,
+    /// or a `rusqlite::Error` if an error occurs during the execution.
+    pub fn build(&mut self, conn: &mut Connection) -> Result<Vec<T>, Error> {
+        let final_query = self.build_query();
+
+        info!("{}", final_query);
+        println!("{}", final_query);
+
+        let mut stmt = conn.statement(&final_query).build()?;
+        let rows = stmt.query(&[])?;
+
+        let mut results: Vec<T> = Vec::new();
+
+        let mut columns: Vec<String> = Vec::new();
+
+        for info in rows.column_info() {
+            columns.push(info.name().to_string().to_lowercase());
+        }
+
+        for row_result in rows {
+            let mut instance = T::default();
+
+            // print column values
+            for (idx, val) in row_result?.sql_values().iter().enumerate() {
+                let column_value_str = match val.oracle_type().unwrap() {
+                    _ => format!("{}", val),
+                };
+
+                let col = columns[idx].clone();
+
+                instance.set_column_value(col.to_lowercase().as_str(), &column_value_str);
+            }
+
+            results.push(instance);
+
+            println!();
+        }
+
+        Ok(results)
+    }
+}
+
+/// Implement `QueryBuilder` for `SelectQueryBuilder`
+///
+/// The where statement ensures the T is long lived
+impl<'a, T> QueryBuilder<'a> for SelectQueryBuilder<'a, T>
+where
+    T: Table + Default + Clone + 'a, // Added 'a bound here
+{
+    fn to_sql(&self) -> String {
+        self.build_query()
+    }
+}
diff --git a/njord/src/oracle/update.rs b/njord/src/oracle/update.rs
new file mode 100644
index 00000000..484f76b0
--- /dev/null
+++ b/njord/src/oracle/update.rs
@@ -0,0 +1,228 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use std::collections::HashMap;
+
+use crate::{
+    condition::Condition,
+    oracle::util::{
+        generate_limit_str, generate_offset_str, generate_order_by_str,
+        generate_where_condition_str, remove_quotes_and_backslashes,
+    },
+};
+
+use log::info;
+use oracle::Connection;
+
+use crate::table::Table;
+
+use super::select::SelectQueryBuilder;
+
+/// Constructs a new UPDATE query builder.
+///
+/// # Arguments
+///
+/// * `conn` - A `Connection` to the Oracle database.
+/// * `table` - An instance of the table to be updated.
+///
+/// # Returns
+///
+/// An `UpdateQueryBuilder` instance.
+pub fn update<T: Table + Default>(conn: &mut Connection, table: T) -> UpdateQueryBuilder<T> {
+    UpdateQueryBuilder::new(conn, table)
+}
+
+/// A builder for constructing UPDATE queries.
+pub struct UpdateQueryBuilder<'a, T: Table + Default> {
+    conn: &'a mut Connection,
+    table: Option<T>,
+    columns: Vec<String>,
+    sub_queries: HashMap<String, SelectQueryBuilder<'a, T>>,
+    where_condition: Option<Condition<'a>>,
+    order_by: Option<HashMap<Vec<String>, String>>,
+    limit: Option<usize>,
+    offset: Option<usize>,
+}
+
+impl<'a, T: Table + Default> UpdateQueryBuilder<'a, T> {
+    /// Creates a new `UpdateQueryBuilder` instance.
+    ///
+    /// # Arguments
+    ///
+    /// * `conn` - A `Connection` to the Oracle database.
+    /// * `table` - An instance of the table to be updated.
+    pub fn new(conn: &'a mut Connection, table: T) -> Self {
+        UpdateQueryBuilder {
+            conn,
+            table: Some(table),
+            columns: Vec::new(),
+            sub_queries: HashMap::new(),
+            where_condition: None,
+            order_by: None,
+            limit: None,
+            offset: None,
+        }
+    }
+
+    /// Sets the columns and values to be updated.
+    ///
+    /// # Arguments
+    ///
+    /// * `columns` - A vector of strings representing the columns to be updated.
+    pub fn set(mut self, columns: Vec<String>) -> Self {
+        self.columns = columns;
+        self
+    }
+
+    /// Sets the columns and values (as subquery) to be updated.
+    ///
+    /// # Arguments
+    ///     
+    /// * `columns` - A hashmap representing the columns and their subqueries.
+    pub fn set_subqueries(mut self, columns: HashMap<String, SelectQueryBuilder<'a, T>>) -> Self {
+        self.sub_queries = columns;
+        self
+    }
+
+    /// Sets the WHERE clause condition.
+    ///
+    /// # Arguments
+    ///
+    /// * `condition` - The condition to be applied in the WHERE clause.
+    pub fn where_clause(mut self, condition: Condition<'a>) -> Self {
+        self.where_condition = Some(condition);
+        self
+    }
+
+    /// Sets the ORDER BY clause columns and order direction.
+    ///
+    /// # Arguments
+    ///
+    /// * `col_and_order` - A hashmap representing the columns and their order directions.
+    pub fn order_by(mut self, col_and_order: HashMap<Vec<String>, String>) -> Self {
+        self.order_by = Some(col_and_order);
+        self
+    }
+
+    /// Sets the LIMIT clause for the query.
+    ///
+    /// # Arguments
+    ///
+    /// * `count` - The maximum number of rows to be updated.
+    pub fn limit(mut self, count: usize) -> Self {
+        self.limit = Some(count);
+        self
+    }
+
+    /// Sets the OFFSET clause for the query.
+    ///
+    /// # Arguments
+    ///
+    /// * `offset` - The offset from which to start updating rows.
+    pub fn offset(mut self, offset: usize) -> Self {
+        self.offset = Some(offset);
+        self
+    }
+
+    /// Builds and executes the UPDATE query.
+    ///
+    /// # Returns
+    ///
+    /// A `Result` indicating success or failure of the update operation.
+    pub fn build(self) -> Result<(), String> {
+        let table_name = self
+            .table
+            .as_ref()
+            .map(|t| t.get_name().to_string())
+            .unwrap_or("".to_string());
+
+        // Sanitize table name from unwanted quotations or backslashes
+        let table_name_str = remove_quotes_and_backslashes(&table_name);
+
+        // Generate SET clause
+        let set = if let Some(table) = &self.table {
+            let mut set_fields = Vec::new();
+            let fields = table.get_column_fields();
+            let values = table.get_column_values();
+
+            for column in &self.columns {
+                // Check if column exists in the table's fields
+                if let Some(index) = fields.iter().position(|c| column == c) {
+                    let value = values.get(index).cloned().unwrap_or_default();
+                    let formatted_value = if value.is_empty() {
+                        "NULL".to_string()
+                    } else if value.parse::<f64>().is_ok() {
+                        value
+                    } else {
+                        format!("'{}'", value)
+                    };
+                    set_fields.push(format!("{} = {}", column, formatted_value));
+                } else {
+                    // Handle the case when the column doesn't exist in the table
+                    eprintln!("Column '{}' does not exist in the table", column);
+                }
+            }
+
+            // Generate subqueries
+            for (column_name, sub_query) in &self.sub_queries {
+                let formatted_value = format!("({})", sub_query.build_query());
+                set_fields.push(format!("{} = {}", column_name, formatted_value));
+            }
+
+            set_fields.join(", ")
+        } else {
+            String::new()
+        };
+
+        let where_condition_str = generate_where_condition_str(self.where_condition);
+        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);
+
+        // Construct the query based on defined variables above
+        let query = format!(
+            "UPDATE {} SET {} {} {} {}",
+            table_name_str,
+            set,
+            where_condition_str,
+            order_by_str,
+            format!("{} {}", limit_str, offset_str),
+        );
+
+        info!("{}", query);
+        println!("{}", query);
+
+        // Prepare SQL statement
+        let _ = self.conn.execute(query.as_str(), &[]);
+
+        Ok(())
+    }
+}
diff --git a/njord/src/oracle/util.rs b/njord/src/oracle/util.rs
new file mode 100644
index 00000000..76b0df07
--- /dev/null
+++ b/njord/src/oracle/util.rs
@@ -0,0 +1,266 @@
+//! BSD 3-Clause License
+//!
+//! Copyright (c) 2024
+//!     Marcus Cvjeticanin
+//!     Chase Willden
+//!
+//! Redistribution and use in source and binary forms, with or without
+//! modification, are permitted provided that the following conditions are met:
+//!
+//! 1. Redistributions of source code must retain the above copyright notice, this
+//!    list of conditions and the following disclaimer.
+//!
+//! 2. Redistributions in binary form must reproduce the above copyright notice,
+//!    this list of conditions and the following disclaimer in the documentation
+//!    and/or other materials provided with the distribution.
+//!
+//! 3. Neither the name of the copyright holder nor the names of its
+//!    contributors may be used to endorse or promote products derived from
+//!    this software without specific prior written permission.
+//!
+//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use std::collections::HashMap;
+
+use crate::condition::Condition;
+
+/// Generates an SQL WHERE clause string based on the provided condition.
+///
+/// If `condition` is Some, it constructs an SQL WHERE clause string with the specified condition.
+/// If `condition` is None, an empty string is returned.
+///
+/// # Arguments
+///
+/// * `condition` - An Option containing the condition.
+///
+/// # Returns
+///
+/// A String representing the generated SQL WHERE clause.
+pub fn generate_where_condition_str(condition: Option<Condition>) -> String {
+    if let Some(condition) = condition {
+        format!("WHERE {}", condition.build())
+    } else {
+        String::new()
+    }
+}
+
+/// Generates an SQL GROUP BY clause string based on the provided columns.
+///
+/// If `columns` is Some, it constructs an SQL GROUP BY clause string with the specified columns.
+/// If `columns` is None, an empty string is returned.
+///
+/// # Arguments
+///
+/// * `columns` - An Option containing a reference to a vector of column names.
+///
+/// # Returns
+///
+/// A String representing the generated SQL GROUP BY clause.
+pub fn generate_group_by_str(columns: &Option<Vec<String>>) -> String {
+    match columns {
+        Some(columns) => format!("GROUP BY {}", columns.join(", ")),
+        None => String::new(),
+    }
+}
+
+/// Generates an SQL ORDER BY clause string based on the provided `order_by` option.
+///
+/// If `order_by` is Some, it should contain a HashMap where the keys are vectors of column names
+/// and the values are corresponding sort orders (ASC or DESC). This function constructs an SQL
+/// ORDER BY clause string based on the content of the HashMap. If the HashMap is empty, an empty
+/// string is returned.
+///
+/// # Arguments
+///
+/// * `order_by` - An Option containing a HashMap where the keys are vectors of column names and
+///   the values are corresponding sort orders (ASC or DESC).
+///
+/// # Returns
+///
+/// A String representing the generated SQL ORDER BY clause.
+pub fn generate_order_by_str(order_by: &Option<HashMap<Vec<String>, String>>) -> String {
+    let order_by_str = if let Some(order_by) = order_by.as_ref() {
+        let order_by_str: Vec<String> = order_by
+            .iter()
+            .map(|(columns, order)| format!("{} {}", columns.join(", "), order))
+            .collect();
+        if !order_by_str.is_empty() {
+            format!("ORDER BY {}", order_by_str.join(", "))
+        } else {
+            String::new()
+        }
+    } else {
+        String::new()
+    };
+
+    return order_by_str;
+}
+
+/// Generates an SQL LIMIT clause string based on the provided limit count.
+///
+/// If `limit` is Some, it constructs an SQL LIMIT clause string with the specified count.
+/// If `limit` is None, an empty string is returned.
+///
+/// # Arguments
+///
+/// * `limit` - An Option containing the limit count.
+///
+/// # Returns
+///
+/// A String representing the generated SQL LIMIT clause.
+pub fn generate_limit_str(limit: Option<usize>) -> String {
+    limit.map_or(String::new(), |count| format!("LIMIT {}", count))
+}
+
+/// Generates an SQL OFFSET clause string based on the provided offset count.
+///
+/// If `offset` is Some, it constructs an SQL OFFSET clause string with the specified count.
+/// If `offset` is None, an empty string is returned.
+///
+/// # Arguments
+///
+/// * `offset` - An Option containing the offset count.
+///
+/// # Returns
+///
+/// A String representing the generated SQL OFFSET clause.
+pub fn generate_offset_str(offset: Option<usize>) -> String {
+    offset.map_or(String::new(), |offset| format!("OFFSET {}", offset))
+}
+
+/// Generates an SQL HAVING clause string based on the provided group by flag and condition.
+///
+/// If `group_by` is true and `having_condition` is Some, it constructs an SQL HAVING clause string
+/// with the specified condition.
+/// If either `group_by` is false or `having_condition` is None, an empty string is returned.
+///
+/// # Arguments
+///
+/// * `group_by` - An Option indicating whether GROUP BY is present.
+/// * `having_condition` - An Option containing the condition for the HAVING clause.
+///
+/// # Returns
+///
+/// A String representing the generated SQL HAVING clause.
+pub fn generate_having_str(group_by: bool, having_condition: Option<&Condition>) -> String {
+    if group_by && having_condition.is_some() {
+        format!("HAVING {}", having_condition.unwrap().build())
+    } else {
+        String::new()
+    }
+}
+
+/// Removes double quotes and backslashes from a given string.
+///
+/// # Arguments
+///
+/// * `input` - The input string from which double quotes and backslashes will be removed.
+///
+/// # Returns
+///
+/// A String with double quotes and backslashes removed.
+pub fn remove_quotes_and_backslashes(input: &str) -> String {
+    input.replace("\"", "").replace("\\", "")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::condition::{Condition, Value};
+
+    #[test]
+    fn test_generate_where_condition_str() {
+        // Test when condition is Some
+        let condition = Condition::Eq("age".to_string(), Value::Literal("30".to_string()));
+        let _result = generate_where_condition_str(Some(condition)); // TODO: need to fix this later
+                                                                     // assert_eq!(result, format!("WHERE {}", condition.build()));
+
+        // Test when condition is None
+        let result = generate_where_condition_str(None);
+        assert_eq!(result, "");
+    }
+
+    #[test]
+    fn test_generate_group_by_str() {
+        // Test when columns is Some
+        let columns = Some(vec!["name".to_string(), "age".to_string()]);
+        let result = generate_group_by_str(&columns);
+        assert_eq!(result, "GROUP BY name, age");
+
+        // Test when columns is None
+        let result = generate_group_by_str(&None);
+        assert_eq!(result, "");
+    }
+
+    /*#[test]
+    fn test_generate_order_by_str() {
+        // Test when order_by is Some
+        let mut map = HashMap::new();
+        map.insert(vec!["name".to_string()], "ASC".to_string());
+        map.insert(vec!["age".to_string()], "DESC".to_string());
+        let result = generate_order_by_str(&Some(map));
+        assert_eq!(result, "ORDER BY name ASC, age DESC");
+
+        // Test when order_by is None
+        let result = generate_order_by_str(&None);
+        assert_eq!(result, "");
+    }*/
+
+    #[test]
+    fn test_generate_limit_str() {
+        // Test when limit is Some
+        let result = generate_limit_str(Some(10));
+        assert_eq!(result, "LIMIT 10");
+
+        // Test when limit is None
+        let result = generate_limit_str(None);
+        assert_eq!(result, "");
+    }
+
+    #[test]
+    fn test_generate_offset_str() {
+        // Test when offset is Some
+        let result = generate_offset_str(Some(5));
+        assert_eq!(result, "OFFSET 5");
+
+        // Test when offset is None
+        let result = generate_offset_str(None);
+        assert_eq!(result, "");
+    }
+
+    #[test]
+    fn test_generate_having_str() {
+        // Test when group_by is true and having_condition is Some
+        let condition = Condition::Gt("COUNT(age)".to_string(), Value::Literal("5".to_string()));
+        let result = generate_having_str(true, Some(&condition));
+        assert_eq!(result, format!("HAVING {}", condition.build()));
+
+        // Test when group_by is false
+        let result = generate_having_str(false, Some(&condition));
+        assert_eq!(result, "");
+
+        // Test when having_condition is None
+        let result = generate_having_str(true, None);
+        assert_eq!(result, "");
+
+        // Test when both group_by is false and having_condition is None
+        let result = generate_having_str(false, None);
+        assert_eq!(result, "");
+    }
+
+    #[test]
+    fn test_remove_quotes_and_backslashes() {
+        let input = r#""table_name\"""#;
+        let result = remove_quotes_and_backslashes(input);
+        assert_eq!(result, "table_name");
+    }
+}
diff --git a/njord_examples/oracle/Cargo.toml b/njord_examples/oracle/Cargo.toml
new file mode 100644
index 00000000..80492ca0
--- /dev/null
+++ b/njord_examples/oracle/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "oracle"
+publish = false
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.81.0"
+
+[dependencies]
+njord = { version = "0.4.0", path = "../../njord", features = ["oracle"] }
+njord_derive = { version = "0.4.0", path = "../../njord_derive" }
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1.0.210", features = ["derive"] }
+reqwest = { version = "0.12", features = ["json"] }
+serde_json = "1.0.132"
\ No newline at end of file
diff --git a/njord_examples/oracle/README.md b/njord_examples/oracle/README.md
new file mode 100644
index 00000000..84773ab7
--- /dev/null
+++ b/njord_examples/oracle/README.md
@@ -0,0 +1,21 @@
+# Oracle
+
+A Oracle database will need to be spun up. This can be found in the `docker-compose.yml` file.
+
+Run the following command:
+
+```bash
+docker-compose up -d
+```
+
+Additionally, ODPI-C will need to be installed to communicate with the docker container:
+
+https://odpi-c.readthedocs.io/en/latest/user_guide/installation.html#overview
+
+Once the database is up and running, we can run the example.
+
+To run this example:
+
+```bash
+cargo r --bin oracle
+```
diff --git a/njord_examples/oracle/init_scripts/1_create_tables.sql b/njord_examples/oracle/init_scripts/1_create_tables.sql
new file mode 100644
index 00000000..74695f9b
--- /dev/null
+++ b/njord_examples/oracle/init_scripts/1_create_tables.sql
@@ -0,0 +1,24 @@
+-- Step 1: Ensure you are in the correct container (Pluggable Database)
+ALTER SESSION SET CONTAINER=FREEPDB1;
+
+-- Step 2: Create the user in that specific container
+CREATE USER njord_user IDENTIFIED BY njord_password QUOTA UNLIMITED ON USERS;
+
+-- Step 3: Grant privileges to the new user
+GRANT CONNECT, RESOURCE, CREATE TABLE TO njord_user;
+
+-- Step 4: Commit changes to make sure the user is visible
+COMMIT;
+
+CREATE TABLE njord_user.neo (
+    id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
+    neo_id VARCHAR2(255) NOT NULL,
+    neo_reference_id VARCHAR2(255),
+    name VARCHAR2(255),
+    name_limited VARCHAR2(255),
+    designation VARCHAR2(255),
+    nasa_jpl_url VARCHAR2(255),
+    absolute_magnitude_h FLOAT,
+    is_potentially_hazardous_asteroid VARCHAR2(8),
+    is_sentry_object VARCHAR2(8)
+);
diff --git a/njord_examples/oracle/src/main.rs b/njord_examples/oracle/src/main.rs
new file mode 100644
index 00000000..cf3f79d4
--- /dev/null
+++ b/njord_examples/oracle/src/main.rs
@@ -0,0 +1,114 @@
+use std::fmt::Error;
+
+use crate::schema::NearEarthObject;
+use njord::column::Column;
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::oracle;
+use reqwest::header::ACCEPT;
+use schema::NeoId;
+use serde::Deserialize;
+use serde_json::Value;
+
+mod schema;
+
+const API_URL: &str = "https://api.nasa.gov/neo/rest/v1";
+
+#[derive(Debug, Deserialize)]
+pub struct NearEarthObjectResponse {
+    #[serde(rename = "id")]
+    pub neo_id: String,
+    pub neo_reference_id: String,
+    pub name: String,
+    pub name_limited: String,
+    pub designation: String,
+    pub nasa_jpl_url: String,
+    pub absolute_magnitude_h: f64,
+    pub is_potentially_hazardous_asteroid: bool,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Error> {
+    let _ = insert().await;
+    let _ = select();
+
+    Ok(())
+}
+
+fn select() -> Result<(), Box<dyn std::error::Error>> {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string).unwrap();
+
+    let results = oracle::select(vec![Column::Text("id".to_string())])
+        .from(NeoId::default())
+        .build(&mut conn);
+
+    match results {
+        Ok(data) => println!("Selected: {:#?}", data.len()),
+        Err(err) => eprintln!("Error: {}", err),
+    }
+
+    Ok(())
+}
+
+async fn insert() -> Result<(), Box<dyn std::error::Error>> {
+    let neo = get_near_earth_objects(0, 10).await;
+    let mut near_earth_objects: Vec<NearEarthObject> = Vec::new();
+
+    match neo {
+        Ok(data) => {
+            for obj in data["near_earth_objects"].as_array().unwrap() {
+                let response_obj: NearEarthObjectResponse =
+                    serde_json::from_value(obj.clone()).unwrap();
+
+                let near_earth_obj = NearEarthObject {
+                    id: AutoIncrementPrimaryKey::default(), // Auto-generate id
+                    neo_id: response_obj.neo_id,            // Map id to neo_id
+                    neo_reference_id: response_obj.neo_reference_id,
+                    name: response_obj.name,
+                    name_limited: response_obj.name_limited,
+                    designation: response_obj.designation,
+                    nasa_jpl_url: response_obj.nasa_jpl_url,
+                    absolute_magnitude_h: response_obj.absolute_magnitude_h,
+                    is_potentially_hazardous_asteroid: response_obj
+                        .is_potentially_hazardous_asteroid,
+                    is_sentry_object: false, // Set this field as needed
+                };
+                println!("{:#?}", near_earth_obj);
+                near_earth_objects.push(near_earth_obj);
+            }
+        }
+        Err(err) => eprintln!("Error: {}", err),
+    }
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string).unwrap();
+
+    match oracle::insert(&mut conn, near_earth_objects) {
+        Ok(_) => println!("Near Earth Objects inserted successfully"),
+        Err(err) => eprintln!("Error: {}", err),
+    };
+
+    Ok(())
+}
+
+async fn get_near_earth_objects(page: u32, size: u32) -> Result<Value, Box<dyn std::error::Error>> {
+    let client = reqwest::Client::new();
+    let endpoint = format!(
+        "{}/neo/browse?page={}&size={}&api_key=DEMO_KEY",
+        API_URL, page, size
+    );
+
+    let response = client
+        .get(endpoint)
+        .header(ACCEPT, "application/json")
+        .send()
+        .await?;
+
+    let response_text = response.text().await?;
+
+    // println!("response = {:#?}", response_text);
+
+    let v: Value = serde_json::from_str(&response_text)?;
+
+    Ok(v)
+}
diff --git a/njord_examples/oracle/src/schema.rs b/njord_examples/oracle/src/schema.rs
new file mode 100644
index 00000000..0146364f
--- /dev/null
+++ b/njord_examples/oracle/src/schema.rs
@@ -0,0 +1,27 @@
+use njord::keys::AutoIncrementPrimaryKey;
+#[allow(unused_imports)]
+use njord::table::Table;
+use njord_derive::Table;
+use serde::Deserialize;
+
+#[derive(Table, Deserialize, Debug)]
+#[table_name = "neo"]
+pub struct NearEarthObject {
+    pub id: AutoIncrementPrimaryKey<usize>,
+    #[serde(rename = "id")]
+    pub neo_id: String,
+    pub neo_reference_id: String,
+    pub name: String,
+    pub name_limited: String,
+    pub designation: String,
+    pub nasa_jpl_url: String,
+    pub absolute_magnitude_h: f64,
+    pub is_potentially_hazardous_asteroid: bool,
+    pub is_sentry_object: bool,
+}
+
+#[derive(Table, Deserialize, Debug)]
+#[table_name = "neo"]
+pub struct NeoId {
+    pub id: AutoIncrementPrimaryKey<usize>,
+}


Test patch:
diff --git a/njord/db/test/oracle/2_setup.sql b/njord/db/test/oracle/2_setup.sql
new file mode 100644
index 00000000..33c7647f
--- /dev/null
+++ b/njord/db/test/oracle/2_setup.sql
@@ -0,0 +1,52 @@
+-- Step 1: Ensure you are in the correct container (Pluggable Database)
+ALTER SESSION
+SET
+    CONTAINER = FREEPDB1;
+
+-- Step 2: Create the user in that specific container
+CREATE USER njord_user IDENTIFIED BY njord_password QUOTA UNLIMITED ON USERS;
+
+-- Step 3: Grant privileges to the new user
+GRANT CONNECT,
+RESOURCE,
+CREATE TABLE TO njord_user;
+
+-- Step 4: Commit changes to make sure the user is visible
+COMMIT;
+
+-- Table: users
+CREATE TABLE njord_user.users (
+    id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
+    -- Auto incrementing primary key for the user ID
+    username VARCHAR2(255) NOT NULL,
+    -- Username field
+    email VARCHAR2(255) NOT NULL,
+    -- Email field
+    address VARCHAR2(255) -- Address field
+);
+
+-- Table: categories
+CREATE TABLE njord_user.categories (
+    id NUMBER PRIMARY KEY,
+    -- Primary key for categories
+    name VARCHAR2(255) NOT NULL -- Name of the category
+);
+
+-- Table: products
+CREATE TABLE njord_user.products (
+    id NUMBER PRIMARY KEY,
+    -- Primary key for products
+    name VARCHAR2(255) NOT NULL,
+    -- Product name
+    description CLOB,
+    -- Product description (using CLOB for large text)
+    price NUMBER(10, 2) NOT NULL,
+    -- Price with up to two decimal places
+    stock_quantity NUMBER NOT NULL,
+    -- Stock quantity
+    category_id NUMBER NOT NULL,
+    -- Foreign key to categories (one-to-one relationship)
+    discount NUMBER(5, 2) DEFAULT 0.00,
+    -- Discount field with default value
+    CONSTRAINT fk_category FOREIGN KEY (category_id) REFERENCES njord_user.categories(id) -- Foreign key constraint to categories table
+);
\ No newline at end of file
diff --git a/njord/tests/oracle/delete_test.rs b/njord/tests/oracle/delete_test.rs
new file mode 100644
index 00000000..3a28090f
--- /dev/null
+++ b/njord/tests/oracle/delete_test.rs
@@ -0,0 +1,52 @@
+use super::User;
+use njord::condition::{Condition, Value};
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::oracle;
+use std::vec;
+
+#[test]
+fn delete_row() {
+    insert_row();
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::delete(c)
+                .from(User::default())
+                .where_clause(Condition::Eq(
+                    "username".to_string(),
+                    Value::Literal("chasewillden2".to_string()),
+                ))
+                .build();
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to DELETE: {:?}", e);
+        }
+    }
+}
+
+/// Helper function to insert a row to be deleted
+fn insert_row() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let table_row: User = User {
+        id: AutoIncrementPrimaryKey::default(),
+        username: "chasewillden2".to_string(),
+        email: "chase.willden@example.com".to_string(),
+        address: "Some Random Address 1".to_string(),
+    };
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::insert(c, vec![table_row]);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to INSERT: {:?}", e);
+        }
+    }
+}
diff --git a/njord/tests/oracle/insert_test.rs b/njord/tests/oracle/insert_test.rs
new file mode 100644
index 00000000..aeeb7106
--- /dev/null
+++ b/njord/tests/oracle/insert_test.rs
@@ -0,0 +1,27 @@
+use super::User;
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::oracle;
+use std::vec;
+
+#[test]
+fn insert_row() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let table_row: User = User {
+        id: AutoIncrementPrimaryKey::default(),
+        username: "chasewillden".to_string(),
+        email: "chase.willden@example.com".to_string(),
+        address: "Some Random Address 1".to_string(),
+    };
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::insert(c, vec![table_row]);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to INSERT: {:?}", e);
+        }
+    }
+}
diff --git a/njord/tests/oracle/mod.rs b/njord/tests/oracle/mod.rs
new file mode 100644
index 00000000..e1e2f5b4
--- /dev/null
+++ b/njord/tests/oracle/mod.rs
@@ -0,0 +1,68 @@
+mod delete_test;
+mod insert_test;
+mod open_test;
+mod select_joins_test;
+mod select_test;
+mod update_test;
+
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::table::Table;
+use njord_derive::Table;
+
+#[derive(Table, Clone)]
+#[table_name = "users"]
+pub struct User {
+    pub id: AutoIncrementPrimaryKey<usize>,
+    pub username: String,
+    pub email: String,
+    pub address: String,
+}
+
+#[derive(Table, Clone)]
+#[table_name = "users"]
+pub struct UserWithSubQuery {
+    pub id: AutoIncrementPrimaryKey<usize>,
+    pub username: String,
+    pub email: String,
+    pub address: String,
+    pub additional_address: String,
+}
+
+#[derive(Table, Clone)]
+#[table_name = "categories"]
+pub struct Category {
+    pub id: AutoIncrementPrimaryKey<usize>,
+    pub name: String,
+}
+
+#[derive(Table, Clone)]
+#[table_name = "products"]
+pub struct Product {
+    pub id: AutoIncrementPrimaryKey<usize>,
+    pub name: String,
+    pub description: String,
+    pub price: f64,
+    pub stock_quantity: usize,
+    // pub category: Category, // one-to-one relationship
+    pub category_id: usize,
+    pub discount: f64,
+}
+
+#[derive(Table)]
+#[table_name = "users"]
+pub struct UsersWithJoin {
+    username: String,
+    price: f64,
+    name: String,
+}
+
+#[derive(Table)]
+#[table_name = "categories"]
+pub struct CategoryWithJoin {
+    name: String,
+    description: String,
+    price: f64,
+    stock_quantity: usize,
+    discount: f64,
+    category_name: String,
+}
diff --git a/njord/tests/oracle/open_test.rs b/njord/tests/oracle/open_test.rs
new file mode 100644
index 00000000..cbd0cdfc
--- /dev/null
+++ b/njord/tests/oracle/open_test.rs
@@ -0,0 +1,8 @@
+use njord::oracle;
+
+#[test]
+fn open_db() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let conn = oracle::open("njord_user", "njord_password", connection_string);
+    assert!(conn.is_ok());
+}
diff --git a/njord/tests/oracle/select_joins_test.rs b/njord/tests/oracle/select_joins_test.rs
new file mode 100644
index 00000000..912c3adf
--- /dev/null
+++ b/njord/tests/oracle/select_joins_test.rs
@@ -0,0 +1,174 @@
+use njord::condition::Condition;
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::oracle;
+use njord::table::Table;
+use njord::util::JoinType;
+use njord::{column::Column, condition::Value};
+use std::sync::Arc;
+
+use crate::{Category, CategoryWithJoin, Product};
+
+fn insert_mock_data<T: Table + Clone + Default>(table_rows: Vec<T>) {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::insert(c, table_rows);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to INSERT: {:?}", e);
+        }
+    }
+}
+
+fn delete_mock_data<T: Table + Clone + Default>(names: Vec<String>, column: String) {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    match conn {
+        Ok(ref mut c) => {
+            // Transform Vec<String> into Vec<Value>
+            let value_list: Vec<Value> = names
+                .into_iter()
+                .map(Value::Literal) // Wrap each username as a Value::Literal
+                .collect();
+
+            let result = oracle::delete(c)
+                .from(T::default())
+                .where_clause(Condition::In(column, value_list))
+                .build();
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to DELETE: {:?}", e);
+        }
+    }
+}
+
+#[test]
+fn select_inner_join() {
+    insert_mock_data(vec![Category {
+        id: AutoIncrementPrimaryKey::new(Some(1)),
+        name: "select_inner_join_test".to_string(),
+    }]);
+
+    insert_mock_data(vec![Product {
+        id: AutoIncrementPrimaryKey::new(Some(1)),
+        name: "select_inner_join_test".to_string(),
+        description: "select_inner_join_test".to_string(),
+        price: 10.0,
+        stock_quantity: 10,
+        discount: 0.0,
+        category_id: 1,
+    }]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    // Assume we have pre-inserted some data into the users and products tables
+    let columns = vec![
+        Column::Text("categories.name".to_string()),
+        Column::Text("products.name".to_string()),
+        Column::Text("products.price".to_string()),
+    ];
+
+    // Assuming a hypothetical join condition: users.id = products.user_id
+    let join_condition = Condition::Eq(
+        "categories.id".to_string(),
+        Value::Literal("products.category_id".to_string()),
+    );
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(CategoryWithJoin::default())
+                .join(
+                    JoinType::Inner,
+                    Arc::new(Product::default()),
+                    join_condition,
+                )
+                .build(c);
+            match result {
+                Ok(r) => {
+                    // Check the number of results and assert against expected values
+                    assert!(!r.is_empty(), "Expected results, but got none.");
+                    // Further assertions on expected data can be made here based on inserted data
+                }
+                Err(e) => panic!("Failed to SELECT with JOIN: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    }
+
+    delete_mock_data::<Category>(
+        vec!["select_inner_join_test".to_string()],
+        "name".to_string(),
+    );
+
+    delete_mock_data::<Product>(
+        vec!["select_inner_join_test".to_string()],
+        "name".to_string(),
+    );
+}
+
+#[test]
+fn select_left_join() {
+    insert_mock_data(vec![Category {
+        id: AutoIncrementPrimaryKey::new(Some(1)),
+        name: "select_inner_join_test".to_string(),
+    }]);
+
+    insert_mock_data(vec![Product {
+        id: AutoIncrementPrimaryKey::new(Some(1)),
+        name: "select_inner_join_test".to_string(),
+        description: "select_inner_join_test".to_string(),
+        price: 10.0,
+        stock_quantity: 10,
+        discount: 0.0,
+        category_id: 1,
+    }]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    // Assume we have pre-inserted some data into the users and products tables
+    let columns = vec![
+        Column::Text("categories.name".to_string()),
+        Column::Text("products.name".to_string()),
+        Column::Text("products.price".to_string()),
+    ];
+
+    // Assuming a hypothetical join condition: users.id = products.user_id
+    let join_condition = Condition::Eq(
+        "categories.id".to_string(),
+        Value::Literal("products.category_id".to_string()),
+    );
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(CategoryWithJoin::default())
+                .join(JoinType::Left, Arc::new(Product::default()), join_condition)
+                .build(c);
+            match result {
+                Ok(r) => {
+                    // Check the number of results and assert against expected values
+                    assert!(!r.is_empty(), "Expected results, but got none.");
+                    // Further assertions on expected data can be made here based on inserted data
+                }
+                Err(e) => panic!("Failed to SELECT with JOIN: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    }
+
+    delete_mock_data::<Category>(
+        vec!["select_inner_join_test".to_string()],
+        "name".to_string(),
+    );
+
+    delete_mock_data::<Product>(
+        vec!["select_inner_join_test".to_string()],
+        "name".to_string(),
+    );
+}
diff --git a/njord/tests/oracle/select_test.rs b/njord/tests/oracle/select_test.rs
new file mode 100644
index 00000000..e61e77ed
--- /dev/null
+++ b/njord/tests/oracle/select_test.rs
@@ -0,0 +1,722 @@
+use njord::condition::Condition;
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::oracle::{self};
+use njord::{column::Column, condition::Value};
+use std::collections::HashMap;
+
+use crate::{User, UserWithSubQuery};
+
+fn insert_mock_data(table_rows: Vec<User>) {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::insert(c, table_rows);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to INSERT: {:?}", e);
+        }
+    }
+}
+
+fn delete_mock_data(usernames: Vec<String>) {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    match conn {
+        Ok(ref mut c) => {
+            // Transform Vec<String> into Vec<Value>
+            let value_list: Vec<Value> = usernames
+                .into_iter()
+                .map(Value::Literal) // Wrap each username as a Value::Literal
+                .collect();
+
+            let result = oracle::delete(c)
+                .from(User::default())
+                .where_clause(Condition::In("username".to_string(), value_list))
+                .build();
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to DELETE: {:?}", e);
+        }
+    }
+}
+
+#[test]
+fn open_db() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let conn = oracle::open("njord_user", "njord_password", connection_string);
+    assert!(conn.is_ok());
+}
+
+#[test]
+fn insert_row() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let table_row: User = User {
+        id: AutoIncrementPrimaryKey::default(),
+        username: "mjovanc".to_string(),
+        email: "mjovanc@icloud.com".to_string(),
+        address: "Some Random Address 1".to_string(),
+    };
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::insert(c, vec![table_row]);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to INSERT: {:?}", e);
+        }
+    }
+}
+
+#[test]
+fn update() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec!["username".to_string()];
+
+    let condition = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("mjovanc".to_string()),
+    );
+
+    let table_row: User = User {
+        id: AutoIncrementPrimaryKey::<usize>::new(Some(0)),
+        username: "mjovanc".to_string(),
+        email: "mjovanc@icloud.com".to_string(),
+        address: "Some Random Address 1".to_string(),
+    };
+
+    let mut order = HashMap::new();
+    order.insert(vec!["id".to_string()], "DESC".to_string());
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::update(c, table_row)
+                .set(columns)
+                .where_clause(condition)
+                .order_by(order)
+                .limit(4)
+                .offset(0)
+                .build();
+            println!("{:?}", result);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to UPDATE: {:?}", e);
+        }
+    }
+}
+
+#[test]
+fn delete() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let condition = Condition::Eq(
+        "address".to_string(),
+        Value::Literal("Some Random Address 1".to_string()),
+    );
+
+    let mut order = HashMap::new();
+    order.insert(vec!["id".to_string()], "DESC".to_string());
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::delete(c)
+                .from(User::default())
+                .where_clause(condition)
+                .order_by(order)
+                .limit(20)
+                .offset(0)
+                .build();
+            println!("{:?}", result);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to DELETE: {:?}", e);
+        }
+    }
+}
+
+#[test]
+fn select() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_test".to_string(),
+            email: "select_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_test2".to_string(),
+            email: "select_test2@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("id".to_string()),
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+        Column::Text("address".to_string()),
+    ];
+    let condition = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_test".to_string()),
+    );
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(User::default())
+                .where_clause(condition)
+                .build(c);
+
+            match result {
+                Ok(r) => assert_eq!(r.len(), 1),
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec!["select_test".to_string(), "select_test2".to_string()]);
+}
+
+#[test]
+fn select_distinct() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_distinct_test".to_string(),
+            email: "select_distinct_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_distinct_test".to_string(),
+            email: "select_distinct_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_distinct_test2".to_string(),
+            email: "select_distinct_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("id".to_string()),
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+        Column::Text("address".to_string()),
+    ];
+    let condition = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_distinct_test".to_string()),
+    );
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(User::default())
+                .where_clause(condition)
+                .distinct()
+                .build(c);
+
+            match result {
+                Ok(r) => {
+                    // TODO: this test does not work properly since it should return 1 but it seems
+                    // like it returns all rows because id is different. Need to check up on that.
+                    assert_eq!(r.len(), 2);
+                }
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec![
+        "select_distinct_test".to_string(),
+        "select_distinct_test2".to_string(),
+    ]);
+}
+
+#[test]
+fn select_order_by() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_order_by_test".to_string(),
+            email: "select_order_by_test@example.com".to_string(),
+            address: "Some Random Address select_order_by".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_order_by_test2".to_string(),
+            email: "select_order_by_test2@example.com".to_string(),
+            address: "Some Random Address select_order_by".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+    ];
+    let condition = Condition::Eq(
+        "address".to_string(),
+        Value::Literal("Some Random Address select_order_by".to_string()),
+    );
+    let group_by = vec!["username".to_string(), "email".to_string()];
+
+    let mut order_by = HashMap::new();
+    order_by.insert(vec!["email".to_string()], "ASC".to_string());
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(User::default())
+                .where_clause(condition)
+                .order_by(order_by)
+                .group_by(group_by)
+                .build(c);
+
+            match result {
+                Ok(r) => assert_eq!(r.len(), 2),
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec![
+        "select_order_by_test".to_string(),
+        "select_order_by_test2".to_string(),
+    ]);
+}
+
+#[test]
+fn select_group_by() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_group_by_test".to_string(),
+            email: "select_group_by_test@example.com".to_string(),
+            address: "Some Random Address select_group_by".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_group_by_test2".to_string(),
+            email: "select_group_by_test@example.com".to_string(),
+            address: "Some Random Address select_group_by".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+    ];
+    let condition = Condition::Eq(
+        "address".to_string(),
+        Value::Literal("Some Random Address select_group_by".to_string()),
+    );
+    let group_by = vec!["username".to_string(), "email".to_string()];
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(User::default())
+                .where_clause(condition)
+                .group_by(group_by)
+                .build(c);
+
+            match result {
+                Ok(r) => assert_eq!(r.len(), 2),
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec![
+        "select_group_by_test".to_string(),
+        "select_group_by_test2".to_string(),
+    ]);
+}
+
+#[test]
+fn select_having() {
+    insert_mock_data(vec![User {
+        id: AutoIncrementPrimaryKey::default(),
+        username: "select_having_test".to_string(),
+        email: "select_having_test@example.com".to_string(),
+        address: "Some Random Address 1".to_string(),
+    }]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+    ];
+    let condition = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_having_test".to_string()),
+    );
+    let group_by = vec!["username".to_string(), "email".to_string()];
+
+    let mut order_by = HashMap::new();
+    order_by.insert(vec!["email".to_string()], "DESC".to_string());
+
+    let having_condition = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_having_test".to_string()),
+    );
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(User::default())
+                .where_clause(condition)
+                .order_by(order_by)
+                .group_by(group_by)
+                .having(having_condition)
+                .build(c);
+
+            match result {
+                Ok(r) => assert_eq!(r.len(), 1),
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    }
+
+    delete_mock_data(vec!["select_having_test".to_string()]);
+}
+
+#[test]
+fn select_except() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_except_test".to_string(),
+            email: "select_except_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_except_test2".to_string(),
+            email: "select_except_test2@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_except_test3".to_string(),
+            email: "select_except_test3@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("id".to_string()),
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+        Column::Text("address".to_string()),
+    ];
+
+    let condition1 = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_except_test".to_string()),
+    );
+    let condition2 = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_except_test2".to_string()),
+    );
+    let condition3 = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("select_except_test3".to_string()),
+    );
+
+    let query1 = oracle::select(columns.clone())
+        .from(User::default())
+        .where_clause(condition1);
+
+    let query2 = oracle::select(columns.clone())
+        .from(User::default())
+        .where_clause(condition2);
+
+    let query3 = oracle::select(columns.clone())
+        .from(User::default())
+        .where_clause(condition3);
+
+    match conn {
+        Ok(ref mut c) => {
+            // Test a chain of EXCEPT queries (query1 EXCEPT query2 EXCEPT query3)
+            let result = query1.except(query2).except(query3).build(c);
+
+            match result {
+                Ok(r) => {
+                    assert_eq!(r.len(), 1, "Expected 1 results after EXCEPT clauses.");
+                }
+                Err(e) => panic!("Failed to SELECT with EXCEPT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec![
+        "select_except_test".to_string(),
+        "select_except_test2".to_string(),
+        "select_except_test3".to_string(),
+    ]);
+}
+
+// #[test]
+// fn select_union() {
+//     insert_mock_data(vec![
+//         User {
+//             id: AutoIncrementPrimaryKey::default(),
+//             username: "select_union_test".to_string(),
+//             email: "select_union_test@example.com".to_string(),
+//             address: "Some Random Address 1".to_string(),
+//         },
+//         User {
+//             id: AutoIncrementPrimaryKey::default(),
+//             username: "select_union_test2".to_string(),
+//             email: "select_union_test2@example.com".to_string(),
+//             address: "Some Random Address 1".to_string(),
+//         },
+//         User {
+//             id: AutoIncrementPrimaryKey::default(),
+//             username: "select_union_test3".to_string(),
+//             email: "select_union_test3@example.com".to_string(),
+//             address: "Some Random Address 1".to_string(),
+//         },
+//     ]);
+
+//     let connection_string = "//localhost:1521/FREEPDB1";
+//     let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+//     let columns = vec![
+//         Column::Text("id".to_string()),
+//         Column::Text("username".to_string()),
+//         Column::Text("email".to_string()),
+//         Column::Text("address".to_string()),
+//     ];
+
+//     let condition1 = Condition::Eq(
+//         "username".to_string(),
+//         Value::Literal("select_union_test".to_string()),
+//     );
+//     let condition2 = Condition::Eq(
+//         "username".to_string(),
+//         Value::Literal("select_union_test2".to_string()),
+//     );
+
+//     let query1 = oracle::select(columns.clone())
+//         .from(User::default())
+//         .where_clause(condition1);
+
+//     let query2 = oracle::select(columns.clone())
+//         .from(User::default())
+//         .where_clause(condition2);
+
+//     match conn {
+//         Ok(ref mut c) => {
+//             // Test a chain of UNION queries (query1 UNION query2)
+//             let result = query1.union(query2).build(c);
+
+//             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].username,
+//                         "select_union_test".to_string(),
+//                         "First user should be mjovanc."
+//                     );
+//                     assert_eq!(
+//                         r[1].username,
+//                         "select_union_test2".to_string(),
+//                         "Second user should be otheruser."
+//                     );
+//                 }
+//                 Err(e) => panic!("Failed to SELECT with UNION: {:?}", e),
+//             };
+//         }
+//         Err(e) => panic!("Failed to SELECT: {:?}", e),
+//     }
+
+//     delete_mock_data(vec![
+//         "select_union_test".to_string(),
+//         "select_union_test2".to_string(),
+//         "select_union_test3".to_string(),
+//     ]);
+// }
+
+#[test]
+fn select_sub_queries() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_sub_queries_test".to_string(),
+            email: "select_sub_queries_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_sub_queries_test2".to_string(),
+            email: "select_sub_queries_test2@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_sub_queries_test3".to_string(),
+            email: "select_sub_queries_test3@example.com".to_string(),
+            address: "SubQuery".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    match conn {
+        Ok(ref mut c) => {
+            let sub_query = oracle::select(vec![Column::Text("address".to_string())])
+                .from(UserWithSubQuery::default())
+                .where_clause(Condition::Eq(
+                    "username".to_string(),
+                    Value::Literal("select_sub_queries_test3".to_string()),
+                ));
+
+            let columns = vec![
+                Column::Text("id".to_string()),
+                Column::Text("username".to_string()),
+                Column::Text("email".to_string()),
+                Column::Text("address".to_string()),
+                Column::SubQuery(Box::new(sub_query), "additional_address".to_string()),
+            ];
+
+            let result = oracle::select(columns)
+                .from(UserWithSubQuery::default())
+                .where_clause(Condition::In(
+                    "username".to_string(),
+                    vec![
+                        Value::Literal("select_sub_queries_test".to_string()),
+                        Value::Literal("select_sub_queries_test2".to_string()),
+                    ],
+                ))
+                .build(c);
+
+            match result {
+                Ok(r) => {
+                    assert!(r.len() > 0);
+                    assert_eq!(r[0].additional_address, "SubQuery");
+                }
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec![
+        "select_sub_queries_test".to_string(),
+        "select_sub_queries_test2".to_string(),
+        "select_sub_queries_test3".to_string(),
+    ]);
+}
+
+#[test]
+fn select_in() {
+    insert_mock_data(vec![
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_in_test".to_string(),
+            email: "select_in_test@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_in_test2".to_string(),
+            email: "select_in_test2@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+        User {
+            id: AutoIncrementPrimaryKey::default(),
+            username: "select_in_test3".to_string(),
+            email: "select_in_test3@example.com".to_string(),
+            address: "Some Random Address 1".to_string(),
+        },
+    ]);
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec![
+        Column::Text("id".to_string()),
+        Column::Text("username".to_string()),
+        Column::Text("email".to_string()),
+        Column::Text("address".to_string()),
+    ];
+
+    let condition = Condition::And(
+        Box::new(Condition::In(
+            "username".to_string(),
+            vec![
+                Value::Literal("select_in_test".to_string()),
+                Value::Literal("select_in_test2".to_string()),
+            ],
+        )),
+        Box::new(Condition::NotIn(
+            "username".to_string(),
+            vec![Value::Literal("select_in_test3".to_string())],
+        )),
+    );
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::select(columns)
+                .from(User::default())
+                .where_clause(condition)
+                .build(c);
+
+            match result {
+                Ok(r) => assert_eq!(r.len(), 2),
+                Err(e) => panic!("Failed to SELECT: {:?}", e),
+            };
+        }
+        Err(e) => panic!("Failed to SELECT: {:?}", e),
+    };
+
+    delete_mock_data(vec![
+        "select_in_test".to_string(),
+        "select_in_test2".to_string(),
+        "select_in_test3".to_string(),
+    ]);
+}
diff --git a/njord/tests/oracle/update_test.rs b/njord/tests/oracle/update_test.rs
new file mode 100644
index 00000000..f12c91dd
--- /dev/null
+++ b/njord/tests/oracle/update_test.rs
@@ -0,0 +1,57 @@
+use super::User;
+use njord::condition::{Condition, Value};
+use njord::keys::AutoIncrementPrimaryKey;
+use njord::oracle;
+use std::vec;
+
+#[test]
+fn delete_row() {
+    insert_row();
+
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let columns = vec!["username".to_string()];
+    let condition = Condition::Eq(
+        "username".to_string(),
+        Value::Literal("chasewillden2".to_string()),
+    );
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::update(c, User::default())
+                .set(columns)
+                .where_clause(condition)
+                .limit(4)
+                .offset(0)
+                .build();
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to DELETE: {:?}", e);
+        }
+    }
+}
+
+/// Helper function to insert a row to be updated
+fn insert_row() {
+    let connection_string = "//localhost:1521/FREEPDB1";
+    let mut conn = oracle::open("njord_user", "njord_password", connection_string);
+
+    let table_row: User = User {
+        id: AutoIncrementPrimaryKey::default(),
+        username: "chasewillden2".to_string(),
+        email: "chase.willden@example.com".to_string(),
+        address: "Some Random Address 1".to_string(),
+    };
+
+    match conn {
+        Ok(ref mut c) => {
+            let result = oracle::insert(c, vec![table_row]);
+            assert!(result.is_ok());
+        }
+        Err(e) => {
+            panic!("Failed to INSERT: {:?}", e);
+        }
+    }
+}