diff --git a/sqlx-core/src/arguments.rs b/sqlx-core/src/arguments.rs index 8867176264..46acd3f76b 100644 --- a/sqlx-core/src/arguments.rs +++ b/sqlx-core/src/arguments.rs @@ -3,6 +3,7 @@ use crate::database::{Database, HasArguments}; use crate::encode::Encode; use crate::types::Type; +use std::fmt::{self, Write}; /// A tuple of arguments to be sent to the database. pub trait Arguments<'q>: Send + Sized + Default { @@ -16,6 +17,10 @@ pub trait Arguments<'q>: Send + Sized + Default { fn add(&mut self, value: T) where T: 'q + Send + Encode<'q, Self::Database> + Type; + + fn format_placeholder(&self, writer: &mut W) -> fmt::Result { + writer.write_str("?") + } } pub trait IntoArguments<'q, DB: HasArguments<'q>>: Sized + Send { diff --git a/sqlx-core/src/lib.rs b/sqlx-core/src/lib.rs index 19dc0f055d..b1143bf97d 100644 --- a/sqlx-core/src/lib.rs +++ b/sqlx-core/src/lib.rs @@ -67,6 +67,7 @@ mod io; mod logger; mod net; pub mod query_as; +pub mod query_builder; pub mod query_scalar; pub mod row; pub mod type_info; diff --git a/sqlx-core/src/postgres/arguments.rs b/sqlx-core/src/postgres/arguments.rs index 9bd60dbbbd..8a210c4691 100644 --- a/sqlx-core/src/postgres/arguments.rs +++ b/sqlx-core/src/postgres/arguments.rs @@ -1,3 +1,4 @@ +use std::fmt::{self, Write}; use std::ops::{Deref, DerefMut}; use crate::arguments::Arguments; @@ -116,6 +117,10 @@ impl<'q> Arguments<'q> for PgArguments { { self.add(value) } + + fn format_placeholder(&self, writer: &mut W) -> fmt::Result { + write!(writer, "${}", self.buffer.count) + } } impl PgArgumentBuffer { diff --git a/sqlx-core/src/query_builder.rs b/sqlx-core/src/query_builder.rs new file mode 100644 index 0000000000..eeb3488584 --- /dev/null +++ b/sqlx-core/src/query_builder.rs @@ -0,0 +1,175 @@ +use std::fmt::Display; +use std::fmt::Write; + +use crate::arguments::Arguments; +use crate::database::{Database, HasArguments}; +use crate::encode::Encode; +use crate::query::Query; +use crate::types::Type; +use either::Either; +use std::marker::PhantomData; + +pub struct QueryBuilder<'a, DB> +where + DB: Database, +{ + query: String, + arguments: Option<>::Arguments>, +} + +impl<'a, DB: Database> QueryBuilder<'a, DB> +where + DB: Database, +{ + pub fn new(init: impl Into) -> Self + where + >::Arguments: Default, + { + QueryBuilder { + query: init.into(), + arguments: Some(Default::default()), + } + } + + pub fn push(&mut self, sql: impl Display) -> &mut Self { + if self.arguments.is_none() { + panic!("QueryBuilder must be reset before reuse") + } + + write!(self.query, "{}", sql).expect("error formatting `sql`"); + + self + } + + pub fn push_bind(&mut self, value: A) -> &mut Self + where + A: 'a + Encode<'a, DB> + Send + Type, + { + match self.arguments { + Some(ref mut arguments) => { + arguments.add(value); + + arguments + .format_placeholder(&mut self.query) + .expect("error in format_placeholder"); + } + None => panic!("Arguments taken already"), + } + + self + } + + pub fn build(&mut self) -> Query<'_, DB, >::Arguments> { + Query { + statement: Either::Left(&self.query), + arguments: match self.arguments.take() { + Some(arguments) => Some(arguments), + None => None, + }, + database: PhantomData, + persistent: true, + } + } + + pub fn reset(&mut self) -> &mut Self { + self.query.clear(); + self.arguments = Some(Default::default()); + + self + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::postgres::Postgres; + + #[test] + fn test_new() { + let qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT * FROM users"); + assert_eq!(qb.query, "SELECT * FROM users"); + } + + #[test] + fn test_push() { + let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT * FROM users"); + let second_line = " WHERE last_name LIKE '[A-N]%;"; + qb.push(second_line); + + assert_eq!( + qb.query, + "SELECT * FROM users WHERE last_name LIKE '[A-N]%;".to_string(), + ); + } + + #[test] + #[should_panic] + fn test_push_panics_when_no_arguments() { + let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT * FROM users;"); + qb.arguments = None; + + qb.push("SELECT * FROM users;"); + } + + #[test] + fn test_push_bind() { + let mut qb: QueryBuilder<'_, Postgres> = + QueryBuilder::new("SELECT * FROM users WHERE id = "); + + qb.push_bind(42i32) + .push(" OR membership_level = ") + .push_bind(3i32); + + assert_eq!( + qb.query, + "SELECT * FROM users WHERE id = $1 OR membership_level = $2" + ); + } + + #[test] + fn test_build() { + let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new("SELECT * FROM users"); + + qb.push(" WHERE id = ").push_bind(42i32); + let query = qb.build(); + + assert_eq!( + query.statement.unwrap_left(), + "SELECT * FROM users WHERE id = $1" + ); + assert_eq!(query.persistent, true); + } + + #[test] + fn test_reset() { + let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new(""); + + let _query = qb + .push("SELECT * FROM users WHERE id = ") + .push_bind(42i32) + .build(); + + qb.reset(); + + assert_eq!(qb.query, ""); + } + + #[test] + fn test_query_builder_reuse() { + let mut qb: QueryBuilder<'_, Postgres> = QueryBuilder::new(""); + + let _query = qb + .push("SELECT * FROM users WHERE id = ") + .push_bind(42i32) + .build(); + + qb.reset(); + + let query = qb.push("SELECT * FROM users WHERE id = 99").build(); + + assert_eq!( + query.statement.unwrap_left(), + "SELECT * FROM users WHERE id = 99" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9d00dfcfdc..2bee5788ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub use sqlx_core::from_row::FromRow; pub use sqlx_core::pool::{self, Pool}; pub use sqlx_core::query::{query, query_with}; pub use sqlx_core::query_as::{query_as, query_as_with}; +pub use sqlx_core::query_builder::QueryBuilder; pub use sqlx_core::query_scalar::{query_scalar, query_scalar_with}; pub use sqlx_core::row::Row; pub use sqlx_core::statement::Statement;