From eb838306ec79cf7406aa1c39173a9f15238beb87 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Fri, 19 Jul 2019 12:45:18 -0400 Subject: [PATCH] Misc bugfixes, doc updates --- src/drivers.rs | 4 +- src/drivers/postgres.rs | 30 +++-- src/lib.rs | 7 +- src/query_builder.rs | 265 +++++++++++++++++++++++++++++--------- src/types.rs | 9 +- tests/integration_test.rs | 6 +- 6 files changed, 233 insertions(+), 88 deletions(-) diff --git a/src/drivers.rs b/src/drivers.rs index 4729b93..51f9cff 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -102,7 +102,9 @@ pub trait DatabaseDriver { } // Runs a basic sql query on the database - // fn query(&self, sql: &str) -> Result; + fn query(&self, sql: &str) -> Result, Box> { + Ok(Box::new(String::from(sql))) + } /// Prepares an sql statement for the database fn prepare(&self, sql: &str) -> Result<(), ()> { diff --git a/src/drivers/postgres.rs b/src/drivers/postgres.rs index 77a3ce3..97d4675 100644 --- a/src/drivers/postgres.rs +++ b/src/drivers/postgres.rs @@ -33,18 +33,6 @@ impl PostgresDriver { self.connection = RefCell::new(Some(connection)); } - - pub fn query(&self, sql: &str) -> Result, Error> { - if self.connection.borrow().is_none() { - panic!("No database connection."); - } - - self.connection - .borrow_mut() - .as_mut() - .unwrap() - .query(sql, &[]) - } } impl DatabaseDriver for PostgresDriver { @@ -55,4 +43,22 @@ impl DatabaseDriver for PostgresDriver { fn random(&self) -> String { String::from(" RANDOM()") } + + fn query(&self, sql: &str) -> Result, Box> { + if self.connection.borrow().is_none() { + panic!("No database connection."); + } + + let result = self + .connection + .borrow_mut() + .as_mut() + .unwrap() + .query(sql, &[]); + + match result { + Ok(res) => Ok(Box::new(res)), + Err(e) => Err(Box::new(e)), + } + } } diff --git a/src/lib.rs b/src/lib.rs index 536ec39..11dac3d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,12 +35,7 @@ pub mod prelude { //! This includes enum types, traits, //! the Query Builder, and individual database drivers. pub use crate::drivers::DatabaseDriver; - pub use crate::query_builder::{ - JoinType, - LikeWildcard, - OrderDirection, - QueryBuilder, - }; + pub use crate::query_builder::{JoinType, LikeWildcard, OrderDirection, QueryBuilder}; #[cfg(feature = "postgres")] /// Postgres Driver diff --git a/src/query_builder.rs b/src/query_builder.rs index 72468cb..5285591 100644 --- a/src/query_builder.rs +++ b/src/query_builder.rs @@ -79,7 +79,36 @@ enum QueryType { Delete, } -/// The struct representing a query builder +/// QueryBuilder for general SQL queries +/// +/// ``` +/// use stringqb::prelude::*; +/// +/// // You probably do not want to use the default driver, as it +/// // is basically a mock for testing +/// use stringqb::drivers::DefaultDriver; +/// +/// // The query builder must be mutable to be useful +/// let mut qb = QueryBuilder::new(DefaultDriver::new()); +/// +/// // Each builder method returns a mutable reference to itself so +/// // the methods are chainable +/// qb.select("field as f") +/// .from("table t") +/// .inner_join("table_two tt", "field2 as ff", "=", "f") +/// .wher("f >", 3); +/// +/// // Since they are references, you do not have to chain. +/// let sql = qb.get_compiled_select(); +/// +/// # assert_eq!( +/// # sql, +/// # r#"SELECT "field" AS "f" +/// FROM "table" "t" +/// INNER JOIN "table_two" "tt" ON "field2" AS "ff"=f +/// WHERE "f" > ?"# +/// # ); +/// ``` pub struct QueryBuilder { /// The struct to store the query builder info state: QueryState, @@ -87,9 +116,7 @@ pub struct QueryBuilder { } /// The struct representing a prepared statement -pub struct Prepared { - -} +pub struct Prepared {} impl Default for QueryBuilder { /// Creates a new QueryBuilder instance with default driver @@ -109,19 +136,19 @@ impl QueryBuilder { /// ```no_run /// use stringqb::prelude::*; /// - /// // You probably do not want to use the default driver, as it - /// // is basically a mock for testing - /// use stringqb::drivers::DefaultDriver; + /// // Postgres Driver (If enabled) + /// let pgDriver = PostgresDriver::new("postgres://"); /// - /// // The query builder must be mutable to be useful - /// let mut qb = QueryBuilder::new(DefaultDriver::new()); + /// #[cfg(feature = "sqlite")] + /// // SQLite Driver (memory) + /// let liteMemoryDriver = SQLiteDriver::new(":memory:"); /// - /// // Each builder method returns a mutable reference to itself so - /// // the methods are chainable - /// qb.select("field f").from("table"); + /// #[cfg(feature = "sqlite")] + /// // SQLite Driver (file) + /// let liteDriver = SQLiteDriver::new("/path/to/db.sqlite3"); /// - /// // Since they are references, you do not have to chain. - /// let sql = qb.get_compiled_select(); + /// // The variable to the query builder must be mutable + /// let mut queryBuilder = QueryBuilder::new(pgDriver); /// ``` pub fn new(driver: impl DatabaseDriver + 'static) -> Self { QueryBuilder { @@ -137,28 +164,13 @@ impl QueryBuilder { /// Set the fields to select from the database as a string /// /// ```no_run - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); /// // You can also alias field names /// qb.select("foo as bar"); /// ``` pub fn select(&mut self, fields: &str) -> &mut Self { - lazy_static! { - static ref RE: Regex = Regex::new(r"(?i) as ").unwrap(); - }; - - let fields = split_map_join(fields, ",", |s| { - if !RE.is_match(s) { - return self.driver.quote_identifier(s.trim()); - } - - // Do a operation similar to split_map_join for the - // regex matches, quoting each identifier - RE.split(s) - .into_iter() - .map(|p| self.driver.quote_identifier(p)) - .collect::>() - .join(" as ") - }); + let fields = self.quote_fields(fields); self.state.append_select_string(&fields); @@ -168,7 +180,8 @@ impl QueryBuilder { /// Set the fields to select from the database as a Vector /// /// ```no_run - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); /// // You can also alias via a vector of fields /// qb.select_vec(vec!["foo as bar", "baz"]); /// ``` @@ -195,8 +208,9 @@ impl QueryBuilder { /// Specify the database table to select from /// /// ```no_run - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); - /// // You can specifiy an alias for the table + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // You can specify an alias for the table /// qb.from("products p"); /// ``` pub fn from(&mut self, table_name: &str) -> &mut Self { @@ -215,7 +229,7 @@ impl QueryBuilder { /// /// ```no_run /// # use stringqb::prelude::*; - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); + /// # let mut qb = QueryBuilder::default(); /// // Search for a value that ends with "foo" /// qb.like("field", String::from("foo"), LikeWildcard::Before); /// @@ -233,7 +247,7 @@ impl QueryBuilder { /// /// ```no_run /// # use stringqb::prelude::*; - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); + /// # let mut qb = QueryBuilder::default(); /// // Search for a value that ends with "foo" /// qb.or_like("field", String::from("foo"), LikeWildcard::Before); /// @@ -251,7 +265,7 @@ impl QueryBuilder { /// /// ```no_run /// # use stringqb::prelude::*; - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); + /// # let mut qb = QueryBuilder::default(); /// // Search for a value that does not end with "foo" /// qb.not_like("field", String::from("foo"), LikeWildcard::Before); /// @@ -269,7 +283,7 @@ impl QueryBuilder { /// /// ```no_run /// # use stringqb::prelude::*; - /// # let mut qb = stringqb::query_builder::QueryBuilder::default(); + /// # let mut qb = QueryBuilder::default(); /// // Search for a value that does not end with "foo" /// qb.or_not_like("field", String::from("foo"), LikeWildcard::Before); /// @@ -393,16 +407,37 @@ impl QueryBuilder { } /// Specify a `where in` clause for the query, prefixed with `or` + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // Look for a set of rows matching the values passed + /// qb.or_where_in("key", vec![1,2,3]); + /// ``` pub fn or_where_in(&mut self, key: &str, values: Vec) -> &mut Self { self._where_in(key, values, "IN", "OR") } /// Specify a `where not in` clause for the query + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // Look for a set of rows not matching the values passed + /// qb.where_not_in("key", vec![1,2,3]); + /// ``` pub fn where_not_in(&mut self, key: &str, values: Vec) -> &mut Self { self._where_in(key, values, "NOT IN", "AND") } /// Specify a `where not in` clause for the query, prefixed with `or` + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // Look for a set of rows not matching the values passed + /// qb.or_where_not_in("key", vec![1,2,3]); + /// ``` pub fn or_where_not_in(&mut self, key: &str, values: Vec) -> &mut Self { self._where_in(key, values, "NOT IN", "OR") } @@ -412,6 +447,15 @@ impl QueryBuilder { // -------------------------------------------------------------------------- /// Set a key and value for an insert or update query + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // Can be called multiple times to set multiple values + /// qb.set("foo", 3) + /// .set("bar", 4) + /// .insert("table"); + /// ``` pub fn set(&mut self, key: &str, value: impl Any) -> &mut Self { let key = self.driver.quote_identifier(key); self.state @@ -431,11 +475,25 @@ impl QueryBuilder { } /// Convenience method for a `left` join + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // Note that the value is not prepared/escaped, due to it often being a column + /// qb.left_join("table1", "column1", "<>", "foo"); + /// ``` pub fn left_join(&mut self, table: &str, col: &str, op: &str, value: &str) -> &mut Self { self.join(table, col, op, value, JoinType::Left) } /// Convenience method for an `inner` join + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // Note that the value is not prepared/escaped, due to it often being a column + /// qb.inner_join("table1", "column1", "<>", "foo"); + /// ``` pub fn inner_join(&mut self, table: &str, col: &str, op: &str, value: &str) -> &mut Self { self.join(table, col, op, value, JoinType::Inner) } @@ -445,7 +503,7 @@ impl QueryBuilder { /// ```no_run /// # use stringqb::prelude::*; /// # let mut qb = QueryBuilder::default(); - /// // Note that the value is not escaped, due to it often being a column + /// // Note that the value is not prepared/escaped, due to it often being a column /// qb.join("table1", "column1", "<>", "foo", JoinType::Inner); /// ``` pub fn join( @@ -456,9 +514,16 @@ impl QueryBuilder { value: &str, join_type: JoinType, ) -> &mut Self { - let table = self.driver.quote_identifier(table); - let col = self.driver.quote_identifier(col); - let condition = table + " ON " + &col + op + value; + let col = self.quote_fields(col); + let table = self.quote_table(table); + + let condition = format!( + "{} ON {}{}{}", + table, + &col, + op, + value + ); let join_type = match join_type { JoinType::Cross => "CROSS ", @@ -520,6 +585,11 @@ impl QueryBuilder { /// Add an offset to the query pub fn offset(&mut self, offset: usize) -> &mut Self { + assert!( + self.state.offset.is_some(), + "You must set a limit to set an offset" + ); + self.state.offset = Some(offset); self @@ -531,16 +601,28 @@ impl QueryBuilder { /// Start a logical grouping in the current query pub fn group_start(&mut self) -> &mut Self { + let conj = if ! self.state.has_where_clause() { + "\nWHERE " + } else { + " " + }; + self.state - .append_query_map(QueryClauseType::GroupStart, " ", "("); + .append_query_map(QueryClauseType::GroupStart, conj, "("); self } /// Start a logical grouping, prefixed with `not` pub fn not_group_start(&mut self) -> &mut Self { + let conj = if ! self.state.has_where_clause() { + "\nWHERE " + } else { + " AND " + }; + self.state - .append_query_map(QueryClauseType::GroupStart, " AND ", "NOT ("); + .append_query_map(QueryClauseType::GroupStart, conj, "NOT ("); self } @@ -580,7 +662,7 @@ impl QueryBuilder { /// # let mut qb = QueryBuilder::default(); /// // The get() method actually calls the driver to run /// // the SQL query - /// let query = db.select_vec(vec!["foo", "bar"]) + /// let query = qb.select_vec(vec!["foo", "bar"]) /// .from("table t") /// .get(); /// ``` @@ -602,7 +684,7 @@ impl QueryBuilder { /// # let mut qb = QueryBuilder::default(); /// // The insert() method actually calls the driver to run /// // the SQL query - /// let query = db.set("foo", 3) + /// let query = qb.set("foo", 3) /// .insert("table"); /// ``` pub fn insert(&mut self, table: &str) { @@ -618,7 +700,7 @@ impl QueryBuilder { /// # let mut qb = QueryBuilder::default(); /// // The update() method actually calls the driver to run /// // the SQL query - /// let query = db.set("foo", 3) + /// let query = qb.set("foo", 3) /// .wher("foo", 4) /// .update("table"); /// ``` @@ -629,6 +711,15 @@ impl QueryBuilder { } /// Execute the generated delete query + /// + /// ```no_run + /// # use stringqb::prelude::*; + /// # let mut qb = QueryBuilder::default(); + /// // The delete() method actually calls the driver to run + /// // the SQL query + /// let query = qb.wher("foo", 3) + /// .delete("table"); + /// ``` pub fn delete(&mut self, table: &str) { let sql = self.get_compiled_delete(table); @@ -670,14 +761,14 @@ impl QueryBuilder { // -------------------------------------------------------------------------- /// Get a new instance of the query builder - pub fn reset(&mut self) -> &Self { + pub fn reset(&mut self) -> &mut Self { self.state = QueryState::new(); self } /// Execute an SQL query with no parameters - pub fn basic_query(&mut self, sql: &str) { + pub fn basic_query(&mut self, sql: &str) -> Result, Box> { self.driver.query(sql) } @@ -691,6 +782,32 @@ impl QueryBuilder { unimplemented!(); } + /// Quotes table column(s)/field(s) accounting for 'as' aliases + fn quote_fields(&mut self, fields: &str) -> String { + lazy_static! { + static ref RE: Regex = Regex::new(r"(?i) as ").unwrap(); + }; + + split_map_join(fields, ",", |s| { + if !RE.is_match(s) { + return self.driver.quote_identifier(s.trim()); + } + + // Do a operation similar to split_map_join for the + // regex matches, quoting each identifier + RE.split(s) + .into_iter() + .map(|p| self.driver.quote_identifier(p)) + .collect::>() + .join(" AS ") + }) + } + + /// Quotes table(s), accounting for aliases + pub fn quote_table(&mut self, table: &str) -> String { + split_map_join(table, " ", |s| self.driver.quote_identifier(s)) + } + // -------------------------------------------------------------------------- // ! Implementation Details // -------------------------------------------------------------------------- @@ -703,6 +820,12 @@ impl QueryBuilder { like: &str, conj: &str, ) -> &mut Self { + let conj = if ! self.state.has_where_clause() { + "\nWHERE " + } else { + conj + }; + let field = self.driver.quote_identifier(field); let like = format!("{} {} ?", field, like); @@ -775,6 +898,12 @@ impl QueryBuilder { let str = format!("{} {} ({}) ", key, in_str, placeholders.join(",")); + let conj = if ! self.state.has_where_clause() { + "\nWHERE " + } else { + conj + }; + self.state .append_query_map(QueryClauseType::WhereIn, conj, &str); @@ -805,21 +934,29 @@ impl QueryBuilder { item += &item2; + let conj = if self.state.query_map.len() == 0 || ( ! self.state.has_where_clause()) { + String::from("\nWHERE") + } else { + String::from(conj) + }; + let conj = if last_item.is_some() { let last_item = last_item.unwrap(); match last_item.clause_type { QueryClauseType::GroupStart => String::from(""), - _ => format!(" {} ", conj), + _ => format!("{} ", conj), } } else { - format!(" {} ", conj) + format!("{} ", conj) }; self.state .append_query_map(QueryClauseType::Where, &conj, &item); } + + fn compile(&self, query_type: QueryType, table: &str) -> String { // Get the base clause for the query let base_sql = self.compile_type(query_type, &self.driver.quote_identifier(table)); @@ -893,7 +1030,7 @@ impl QueryBuilder { // @TODO determine query result type // @TODO prepare/execute query, and return result let stmt = self.prepare(sql); - self.execute(&stmt, values) + self.execute(&stmt, &values) } } @@ -1041,12 +1178,6 @@ impl QueryState { } fn append_query_map(&mut self, clause_type: QueryClauseType, conj: &str, s: &str) -> &mut Self { - let conj = if self.query_map.len() == 0 { - " WHERE " - } else { - conj - }; - self.query_map.push(QueryClause::new(clause_type, conj, s)); self @@ -1110,6 +1241,20 @@ impl QueryState { &mut self.where_values } + fn has_where_clause(&self) -> bool { + let has_clause = false; + + for clause in self.query_map.iter() { + match clause.clause_type { + QueryClauseType::Where => return true, + QueryClauseType::WhereIn => return true, + _ => (), + } + } + + has_clause + } + fn set_from_string(&mut self, s: &str) -> &mut Self { self.from_string = String::from(s); @@ -1172,7 +1317,7 @@ mod tests { qb.from("test").where_in("foo", vec![0, 1, 2, 3, 4, 5]); let sql = qb.get_compiled_select(); - let expected = "SELECT *\nFROM \"test\" WHERE \"foo\" IN (?,?,?,?,?,?) "; + let expected = "SELECT *\nFROM \"test\"\nWHERE \"foo\" IN (?,?,?,?,?,?) "; assert_eq!(sql, expected); } diff --git a/src/types.rs b/src/types.rs index 573695c..cb5b48b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -86,8 +86,8 @@ pub enum ToDriverOutput<'a> { // Generically allow any type that can be converted into a ValueRef // to be converted into a ToSqlOutput as well. impl<'a, T: ?Sized> From<&'a T> for ToDriverOutput<'a> - where - &'a T: Into>, +where + &'a T: Into>, { fn from(t: &'a T) -> Self { ToDriverOutput::Borrowed(t.into()) @@ -120,16 +120,13 @@ impl<'a, T: ?Sized> From<&'a T> for ToDriverOutput<'a> //from_value!(f64); //from_value!(Vec); - /// Types that can be converted to a type that the driver understands pub trait ToDriver { fn to_driver(&self) -> Result, ()>; } /// A trait for types that can be created from the result of a query on the driver -pub trait FromDriver: Sized { - -} +pub trait FromDriver: Sized {} /// Enum struct for mapping between database and Rust types #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 33edec3..54f8eb8 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -15,7 +15,7 @@ fn select_keys_as_query() { let sql = qb.select("foo as bar, baz").from("a").get_compiled_select(); - assert_eq!(sql, "SELECT \"foo\" as \"bar\",\"baz\"\nFROM \"a\""); + assert_eq!(sql, "SELECT \"foo\" AS \"bar\",\"baz\"\nFROM \"a\""); } #[test] @@ -54,7 +54,7 @@ fn select_where() { qb.from("test").r#where("foo", "bar"); let sql = qb.get_compiled_select(); - let expected = "SELECT *\nFROM \"test\" WHERE \"foo\"=?"; + let expected = "SELECT *\nFROM \"test\"\nWHERE \"foo\"=?"; assert_eq!(sql, expected); } @@ -66,7 +66,7 @@ fn select_where_in() { qb.from("test").where_in("foo", vec![0, 1, 2, 3, 4, 5]); let sql = qb.get_compiled_select(); - let expected = "SELECT *\nFROM \"test\" WHERE \"foo\" IN (?,?,?,?,?,?) "; + let expected = "SELECT *\nFROM \"test\"\nWHERE \"foo\" IN (?,?,?,?,?,?) "; assert_eq!(sql, expected); }