diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index f9c8b939..46185dba 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -37,6 +37,21 @@ jobs: MYSQL_USER: njord_user MYSQL_PASSWORD: njord_password + oracle: + image: gvenzl/oracle-free:latest + options: >- + --health-cmd="healthcheck.sh" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + ports: + - 1521:1521 + - 5500:5500 + env: + ORACLE_PASSWORD: njord_password + APP_USER: test + APP_USER_PASSWORD: test + steps: - uses: actions/checkout@v4 @@ -65,6 +80,7 @@ jobs: - name: Running Integration Tests for SQLite run: cargo test --test sqlite_tests + # MySQL related steps - name: Wait for MySQL to be ready run: | until mysqladmin ping -h 127.0.0.1 --silent; do @@ -74,7 +90,7 @@ 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 @@ -89,3 +105,28 @@ jobs: MYSQL_PASSWORD: njord_password MYSQL_HOST: 127.0.0.1 run: cargo test --test mysql_tests + + # Oracle related steps + - name: Wait for Oracle to be ready + run: | + echo "Waiting for Oracle to be ready..." + until echo "exit" | sqlplus test/test@//127.0.0.1:1521/XEPDB1; do + echo "Waiting for Oracle to be ready..." + sleep 10 + done + + - name: Set up Oracle schema + run: | + echo "Injecting schema and data into Oracle..." + sqlplus test/test@//127.0.0.1:1521/XEPDB1 @njord/db/test/oracle/tests.sql + + - name: Running Unit Tests for Oracle + run: cargo test oracle + + - name: Running Integration Tests for Oracle + env: + ORACLE_DATABASE: XEPDB1 + 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..ec6a0264 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: mysql: image: mysql:8.0 @@ -16,5 +14,27 @@ services: - ./njord_examples/mysql/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/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/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(conn: &mut Connection) -> DeleteQueryBuilder { + DeleteQueryBuilder::new(conn) +} + +/// A builder for constructing DELETE queries. +pub struct DeleteQueryBuilder<'a, T: Table + Default> { + conn: &'a mut Connection, + table: Option, + where_condition: Option>, + order_by: Option, String>>, + limit: Option, + offset: Option, +} + +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, 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 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( + conn: &mut Connection, + table_rows: Vec, +) -> Result { + let mut statements: Vec = 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, + subquery: Box + 'a>, +) -> Result { + let statement = generate_insert_into_statement::(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, + subquery: Box + 'a>, +) -> Result { + 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(table_row: &T, first_statement: bool) -> Result { + // 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 { + 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(columns: Vec) -> SelectQueryBuilder { + SelectQueryBuilder::new(columns) +} + +/// A builder for constructing SELECT queries. +#[derive(Clone)] +pub struct SelectQueryBuilder<'a, T: Table + Default> { + table: Option, + columns: Vec>, + where_condition: Option>, + distinct: bool, + group_by: Option>, + order_by: Option, String>>, + having_condition: Option>, + except_clauses: Option>>, + union_clauses: Option>>, + joins: Option>>, +} + +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>) -> 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>) -> 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) -> 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, 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, + 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::>() + .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 = 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, 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 = Vec::new(); + + let mut columns: Vec = 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(conn: &mut Connection, table: T) -> UpdateQueryBuilder { + UpdateQueryBuilder::new(conn, table) +} + +/// A builder for constructing UPDATE queries. +pub struct UpdateQueryBuilder<'a, T: Table + Default> { + conn: &'a mut Connection, + table: Option, + columns: Vec, + sub_queries: HashMap>, + where_condition: Option>, + order_by: Option, String>>, + limit: Option, + offset: Option, +} + +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) -> 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>) -> 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, 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::().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) -> 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>) -> 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, String>>) -> String { + let order_by_str = if let Some(order_by) = order_by.as_ref() { + let order_by_str: Vec = 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) -> 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) -> 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/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, + pub username: String, + pub email: String, + pub address: String, +} + +#[derive(Table, Clone)] +#[table_name = "users"] +pub struct UserWithSubQuery { + pub id: AutoIncrementPrimaryKey, + 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, + pub name: String, +} + +#[derive(Table, Clone)] +#[table_name = "products"] +pub struct Product { + pub id: AutoIncrementPrimaryKey, + 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(table_rows: Vec) { + 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(names: Vec, 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 into Vec + let value_list: Vec = 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::( + vec!["select_inner_join_test".to_string()], + "name".to_string(), + ); + + delete_mock_data::( + 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::( + vec!["select_inner_join_test".to_string()], + "name".to_string(), + ); + + delete_mock_data::( + 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) { + 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) { + 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 into Vec + let value_list: Vec = 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::::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); + } + } +} 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> { + 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> { + let neo = get_near_earth_objects(0, 10).await; + let mut near_earth_objects: Vec = 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> { + 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, + #[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, +}