Skip to content

Commit

Permalink
Add Query builder (#1780)
Browse files Browse the repository at this point in the history
* Add Query builder

* Make query_builder.rs in sqlx-core

* Add QueryBuilder::new()

* Add QueryBuilder::push()

* Define questions for documentation

* Get new, push, push_bind working with types

* Handle postgres' numbered bind varaibles

* Add a test for QueryBuilder#build

* Move arguments into Option

* Refactor query builder

* Finish testing QueryBuilder#build

* Remove design doc

* Add a test for pushing strings with push_bind

* Integration test green

* Adjust some tests

* Make query builder generic about placeholder segmenent ('$N' or '?')

* Run fmt

* Redesign Arguments#format_placeholder in line with code review

* Use write! to push sql to QueryBuilder

* Add QueryBuilder::reset to allow for QueryBuilder reuse

* Run cargo fmt
  • Loading branch information
crajcan authored Apr 8, 2022
1 parent 6efc39f commit a470682
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 0 deletions.
5 changes: 5 additions & 0 deletions sqlx-core/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,6 +17,10 @@ pub trait Arguments<'q>: Send + Sized + Default {
fn add<T>(&mut self, value: T)
where
T: 'q + Send + Encode<'q, Self::Database> + Type<Self::Database>;

fn format_placeholder<W: Write>(&self, writer: &mut W) -> fmt::Result {
writer.write_str("?")
}
}

pub trait IntoArguments<'q, DB: HasArguments<'q>>: Sized + Send {
Expand Down
1 change: 1 addition & 0 deletions sqlx-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions sqlx-core/src/postgres/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::{self, Write};
use std::ops::{Deref, DerefMut};

use crate::arguments::Arguments;
Expand Down Expand Up @@ -116,6 +117,10 @@ impl<'q> Arguments<'q> for PgArguments {
{
self.add(value)
}

fn format_placeholder<W: Write>(&self, writer: &mut W) -> fmt::Result {
write!(writer, "${}", self.buffer.count)
}
}

impl PgArgumentBuffer {
Expand Down
175 changes: 175 additions & 0 deletions sqlx-core/src/query_builder.rs
Original file line number Diff line number Diff line change
@@ -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<<DB as HasArguments<'a>>::Arguments>,
}

impl<'a, DB: Database> QueryBuilder<'a, DB>
where
DB: Database,
{
pub fn new(init: impl Into<String>) -> Self
where
<DB as HasArguments<'a>>::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<A>(&mut self, value: A) -> &mut Self
where
A: 'a + Encode<'a, DB> + Send + Type<DB>,
{
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, <DB as HasArguments<'a>>::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"
);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit a470682

Please sign in to comment.