Include bind parameters in debug_sql!

I've implemented this as a separate AST pass, rather than the
`CollectBinds` pass of the `Debug` backend. This is because we will
ultimately want to use this same code for logging, which won't have the
`QueryFragment<Debug>` constraint. Ultimately I think I want to get rid
of the `Debug` backend entirely once logging is implemented.

I had originally implemented this by adding two additional lifetime
parameters onto `AstPass`. However, I really disliked how opaque and
random these two extra parameters felt, and it made the type harder to
document (and use). Shortening an invariant lifetime like this is
actually one of the documented use cases that `mem::transmute` is useful
for. I don't like introducing unsafe code in general, but I felt that
the tradeoff was worth it in this case.
This commit is contained in:
Sean Griffin 2017-07-29 07:17:28 -04:00
parent 83efa6664f
commit 47f1710a5c
11 changed files with 228 additions and 65 deletions

View File

@ -10,9 +10,10 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
* Added helper types for inner join and left outer join
* `diesel::debug_sql` has been added as a replacement for `debug_sql!`. This
* `diesel::debug_query` has been added as a replacement for `debug_sql!`. This
function differs from the macro by allowing you to specify the backend, and
will generate the actual query which will be run.
will generate the actual query which will be run. The returned value will
implement `Display` and `Debug` to show the query in different ways
* `diesel::pg::PgConnection`, `diesel::mysql::MysqlConnection`, and
`diesel::sqlite::SqliteConnection` are now exported from `diesel::prelude`.
@ -47,7 +48,7 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
### Removed
* `debug_sql!` has been deprecated in favor of `diesel::debug_sql`.
* `debug_sql!` has been deprecated in favor of `diesel::debug_query`.
* `print_sql!` has been deprecated without replacement.

View File

@ -144,7 +144,7 @@ pub mod prelude {
pub use prelude::*;
#[doc(inline)]
pub use query_builder::debug_sql;
pub use query_builder::debug_query;
#[doc(inline)]
pub use query_builder::functions::{insert, update, delete, select, insert_default_values};
#[cfg(feature = "sqlite")]

View File

@ -999,15 +999,23 @@ macro_rules! enable_multi_table_joins {
/// # fn main() {
/// let sql = debug_sql!(users.count());
/// if cfg!(feature = "postgres") {
/// assert_eq!(sql, r#"SELECT COUNT(*) FROM "users""#);
/// assert_eq!(sql, r#"SELECT COUNT(*) FROM "users" -- binds: []"#);
/// } else {
/// assert_eq!(sql, "SELECT COUNT(*) FROM `users`");
/// assert_eq!(sql, "SELECT COUNT(*) FROM `users` -- binds: []");
/// }
///
/// let sql = debug_sql!(users.filter(n.eq(1)));
/// if cfg!(feature = "postgres") {
/// assert_eq!(sql, r#"SELECT "users"."id", "users"."n" FROM "users" WHERE "users"."n" = $1 -- binds: [1]"#);
/// } else {
/// assert_eq!(sql, "SELECT `users`.`id`, `users`.`n` FROM `users` WHERE \
/// `users`.`n` = ? -- binds: [1]");
/// }
/// # }
/// ```
#[cfg(feature = "with-deprecated")]
#[macro_export]
#[deprecated(since = "0.16.0", note = "use `diesel::debug_sql` instead")]
#[deprecated(since = "0.16.0", note = "use `diesel::debug_query(...).to_string()` instead")]
macro_rules! debug_sql {
($query:expr) => {
$crate::query_builder::deprecated_debug_sql(&$query)
@ -1039,7 +1047,7 @@ macro_rules! debug_sql {
/// ```
#[macro_export]
#[cfg(feature = "with-deprecated")]
#[deprecated(since = "0.16.0", note = "use `println!(\"{}\", diesel::debug_sql(...))` instead")]
#[deprecated(since = "0.16.0", note = "use `println!(\"{}\", diesel::debug_query(...))` instead")]
macro_rules! print_sql {
($query:expr) => {
println!("{}", debug_sql!($query));
@ -1105,8 +1113,8 @@ mod tests {
#[cfg(feature = "postgres")]
fn table_with_custom_schema() {
use pg::Pg;
let expected_sql = r#"SELECT "foo"."bars"."baz" FROM "foo"."bars""#;
assert_eq!(expected_sql, ::debug_sql::<Pg, _>(&bars::table.select(bars::baz)));
let expected_sql = r#"SELECT "foo"."bars"."baz" FROM "foo"."bars" -- binds: []"#;
assert_eq!(expected_sql, &::debug_query::<Pg, _>(&bars::table.select(bars::baz)).to_string());
}
table! {

View File

@ -1,3 +1,5 @@
use std::{fmt, mem};
use backend::Backend;
use query_builder::{BindCollector, QueryBuilder};
use result::QueryResult;
@ -55,6 +57,15 @@ impl<'a, DB> AstPass<'a, DB> where
}
}
#[doc(hidden)]
pub fn debug_binds(
formatter: &'a mut fmt::DebugList<'a, 'a>,
) -> Self {
AstPass {
internals: AstPassInternals::DebugBinds(formatter),
}
}
/// Call this method whenever you pass an instance of `AstPass` by value.
///
/// Effectively copies `self`, with a narrower lifetime. When passing a
@ -75,6 +86,11 @@ impl<'a, DB> AstPass<'a, DB> where
}
}
IsSafeToCachePrepared(ref mut result) => IsSafeToCachePrepared(&mut **result),
DebugBinds(ref mut f) => {
// Safe because the lifetime is always being shortened.
let f_with_shorter_lifetime = unsafe { mem::transmute(&mut **f) };
DebugBinds(f_with_shorter_lifetime)
}
};
AstPass { internals }
}
@ -155,6 +171,9 @@ impl<'a, DB> AstPass<'a, DB> where
ToSql(ref mut out) => out.push_bind_param(),
CollectBinds { ref mut collector, metadata_lookup } =>
collector.push_bound_value(bind, metadata_lookup)?,
DebugBinds(ref mut f) => {
f.entry(bind);
}
_ => {}, // noop
}
Ok(())
@ -176,8 +195,10 @@ impl<'a, DB> AstPass<'a, DB> where
DB: HasSqlType<T>,
U: ToSql<T, DB>,
{
if let AstPassInternals::CollectBinds { .. } = self.internals {
self.push_bind_param(bind)?;
use self::AstPassInternals::*;
match self.internals {
CollectBinds { .. } | DebugBinds(..) => self.push_bind_param(bind)?,
_ => {}
}
Ok(())
}
@ -200,4 +221,5 @@ enum AstPassInternals<'a, DB> where
metadata_lookup: &'a DB::MetadataLookup,
},
IsSafeToCachePrepared(&'a mut bool),
DebugBinds(&'a mut fmt::DebugList<'a, 'a>),
}

View File

@ -0,0 +1,91 @@
use std::fmt::{self, Display, Debug};
use std::marker::PhantomData;
use std::mem;
use backend::Backend;
use super::{QueryBuilder, QueryFragment, AstPass};
/// A struct that implements `fmt::Display` and `fmt::Debug` to show the SQL
/// representation of a query.
///
/// The `Display` implementation will be the exact query sent to the server,
/// plus a comment with the values of the bind parameters. The `Debug`
/// implementation is more structured, and able to be pretty printed.
pub struct DebugQuery<'a, T: 'a, DB> {
query: &'a T,
_marker: PhantomData<DB>,
}
impl<'a, T, DB> DebugQuery<'a, T, DB> {
pub(crate) fn new(query: &'a T) -> Self {
DebugQuery {
query,
_marker: PhantomData,
}
}
}
impl<'a, T, DB> Display for DebugQuery<'a, T, DB> where
DB: Backend,
DB::QueryBuilder: Default,
T: QueryFragment<DB>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut query_builder = DB::QueryBuilder::default();
QueryFragment::<DB>::to_sql(self.query, &mut query_builder)
.map_err(|_| fmt::Error)?;
let debug_binds = DebugBinds::<_, DB>::new(self.query);
write!(f, "{} -- binds: {:?}", query_builder.finish(), debug_binds)
}
}
impl<'a, T, DB> Debug for DebugQuery<'a, T, DB> where
DB: Backend,
DB::QueryBuilder: Default,
T: QueryFragment<DB>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut query_builder = DB::QueryBuilder::default();
QueryFragment::<DB>::to_sql(self.query, &mut query_builder)
.map_err(|_| fmt::Error)?;
let debug_binds = DebugBinds::<_, DB>::new(self.query);
f.debug_struct("Query")
.field("sql", &query_builder.finish())
.field("binds", &debug_binds)
.finish()
}
}
/// A struct that implements `fmt::Debug` by walking the given AST and writing
/// the `fmt::Debug` implementation of each bind parameter.
pub struct DebugBinds<'a, T: 'a, DB> {
query: &'a T,
_marker: PhantomData<DB>,
}
impl<'a, T, DB> DebugBinds<'a, T, DB> {
fn new(query: &'a T) -> Self {
DebugBinds {
query,
_marker: PhantomData,
}
}
}
impl<'a, T, DB> Debug for DebugBinds<'a, T, DB> where
DB: Backend,
T: QueryFragment<DB>,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut list = f.debug_list();
{
// Safe because the lifetime is shortened to one smaller
// than the lifetime of the formatter.
let list_with_shorter_lifetime = unsafe { mem::transmute(&mut list) };
let ast_pass = AstPass::debug_binds(list_with_shorter_lifetime);
self.query.walk_ast(ast_pass).map_err(|_| fmt::Error)?;
}
list.finish()?;
Ok(())
}
}

View File

@ -11,6 +11,7 @@ mod clause_macro;
mod ast_pass;
pub mod bind_collector;
mod debug_query;
mod delete_statement;
#[doc(hidden)]
pub mod functions;
@ -30,6 +31,7 @@ pub mod update_statement;
pub use self::ast_pass::AstPass;
pub use self::bind_collector::BindCollector;
pub use self::debug_query::DebugQuery;
pub use self::query_id::QueryId;
#[doc(hidden)]
pub use self::select_statement::{SelectStatement, BoxedSelectStatement};
@ -148,8 +150,13 @@ impl<T: Query> AsQuery for T {
}
}
/// Takes a query `QueryFragment` expression as an argument and returns a string
/// of SQL with placeholders for the dynamic values.
/// Takes a query `QueryFragment` expression as an argument and returns a type
/// that implements `fmt::Display` and `fmt::Debug` to show the query.
///
/// The `Display` implementation will show the exact query being sent to the
/// server, with a comment showing the values of the bind parameters. The
/// `Debug` implementation will include the same information in a more
/// structured form, and respects pretty printing.
///
/// # Example
///
@ -164,23 +171,36 @@ impl<T: Query> AsQuery for T {
/// #
/// # fn main() {
/// # use schema::users::dsl::*;
/// let sql = debug_sql::<DB, _>(&users.count());
/// if cfg!(feature = "postgres") {
/// assert_eq!(sql, r#"SELECT COUNT(*) FROM "users""#);
/// } else {
/// assert_eq!(sql, "SELECT COUNT(*) FROM `users`");
/// }
/// let sql = debug_query::<DB, _>(&users.count()).to_string();
/// # if cfg!(feature = "postgres") {
/// # assert_eq!(sql, r#"SELECT COUNT(*) FROM "users" -- binds: []"#);
/// # } else {
/// assert_eq!(sql, "SELECT COUNT(*) FROM `users` -- binds: []");
/// # }
///
/// let query = users.find(1);
/// let debug = debug_query::<DB, _>(&query);
/// # if cfg!(feature = "postgres") {
/// # assert_eq!(debug.to_string(), "SELECT \"users\".\"id\", \"users\".\"name\" \
/// # FROM \"users\" WHERE \"users\".\"id\" = $1 -- binds: [1]");
/// # } else {
/// assert_eq!(debug.to_string(), "SELECT `users`.`id`, `users`.`name` FROM `users` \
/// WHERE `users`.`id` = ? -- binds: [1]");
/// # }
///
/// let debug = format!("{:?}", debug);
/// # if !cfg!(feature = "postgres") { // Escaping that string is a pain
/// let expected = "Query { \
/// sql: \"SELECT `users`.`id`, `users`.`name` FROM `users` WHERE \
/// `users`.`id` = ?\", \
/// binds: [1] \
/// }";
/// assert_eq!(debug, expected);
/// # }
/// # }
/// ```
pub fn debug_sql<DB, T>(query: &T) -> String where
DB: Backend,
DB::QueryBuilder: Default,
T: QueryFragment<DB>,
{
let mut query_builder = DB::QueryBuilder::default();
QueryFragment::<DB>::to_sql(query, &mut query_builder)
.expect("Failed to construct query");
query_builder.finish()
pub fn debug_query<DB, T>(query: &T) -> DebugQuery<T, DB> {
DebugQuery::new(query)
}
#[doc(hidden)]
@ -188,7 +208,7 @@ pub fn debug_sql<DB, T>(query: &T) -> String where
pub fn deprecated_debug_sql<T>(query: &T) -> String where
T: QueryFragment<::pg::Pg>,
{
debug_sql(query)
debug_query(query).to_string()
}
#[doc(hidden)]
@ -196,7 +216,7 @@ pub fn deprecated_debug_sql<T>(query: &T) -> String where
pub fn deprecated_debug_sql<T>(query: &T) -> String where
T: QueryFragment<::mysql::Mysql>,
{
debug_sql(query)
debug_query(query).to_string()
}
#[doc(hidden)]
@ -204,7 +224,7 @@ pub fn deprecated_debug_sql<T>(query: &T) -> String where
pub fn deprecated_debug_sql<T>(query: &T) -> String where
T: QueryFragment<::sqlite::Sqlite>,
{
debug_sql(query)
debug_query(query).to_string()
}
#[doc(hidden)]

View File

@ -59,8 +59,8 @@ fn simple_belongs_to() {
let filter = posts::table.filter(posts::user_id.eq(42));
assert_eq!(
debug_sql::<Backend, _>(&belong_to),
debug_sql::<Backend, _>(&filter)
debug_query::<Backend, _>(&belong_to).to_string(),
debug_query::<Backend, _>(&filter).to_string()
);
}
@ -118,8 +118,8 @@ fn custom_foreign_key() {
let filter = posts::table.filter(posts::belongs_to_user.eq(42));
assert_eq!(
debug_sql::<Backend, _>(&belong_to),
debug_sql::<Backend, _>(&filter)
debug_query::<Backend, _>(&belong_to).to_string(),
debug_query::<Backend, _>(&filter).to_string()
);
}
@ -146,7 +146,7 @@ fn self_referential() {
let belong_to = Tree::belonging_to(&t);
let filter = trees::table.filter(trees::parent_id.eq(42));
assert_eq!(
debug_sql::<Backend, _>(&belong_to),
debug_sql::<Backend, _>(&filter)
debug_query::<Backend, _>(&belong_to).to_string(),
debug_query::<Backend, _>(&filter).to_string()
);
}

View File

@ -4,11 +4,11 @@ use diesel::*;
#[test]
fn test_debug_count_output() {
use schema::users::dsl::*;
let sql = debug_sql::<TestBackend, _>(&users.count());
let sql = debug_query::<TestBackend, _>(&users.count()).to_string();
if cfg!(feature = "postgres") {
assert_eq!(sql, r#"SELECT COUNT(*) FROM "users""#);
assert_eq!(sql, r#"SELECT COUNT(*) FROM "users" -- binds: []"#);
} else {
assert_eq!(sql, "SELECT COUNT(*) FROM `users`");
assert_eq!(sql, "SELECT COUNT(*) FROM `users` -- binds: []");
}
}
@ -16,10 +16,10 @@ fn test_debug_count_output() {
fn test_debug_output() {
use schema::users::dsl::*;
let command = update(users.filter(id.eq(1))).set(name.eq("new_name"));
let sql = debug_sql::<TestBackend, _>(&command);
let sql = debug_query::<TestBackend, _>(&command).to_string();
if cfg!(feature = "postgres") {
assert_eq!(sql, r#"UPDATE "users" SET "name" = $1 WHERE "users"."id" = $2"#)
assert_eq!(sql, r#"UPDATE "users" SET "name" = $1 WHERE "users"."id" = $2 -- binds: ["new_name", 1]"#)
} else {
assert_eq!(sql, "UPDATE `users` SET `name` = ? WHERE `users`.`id` = ?")
assert_eq!(sql, r#"UPDATE `users` SET `name` = ? WHERE `users`.`id` = ? -- binds: ["new_name", 1]"#)
}
}

View File

@ -28,7 +28,7 @@ fn test_count_star() {
assert_eq!(Ok(1), source.first(&connection));
// Ensure we're doing COUNT(*) instead of COUNT(table.*) which is going to be more efficient
assert!(debug_sql::<TestBackend, _>(&source).starts_with("SELECT COUNT(*) FROM"));
assert!(debug_query::<TestBackend, _>(&source).to_string().starts_with("SELECT COUNT(*) FROM"));
}
table! {

View File

@ -9,12 +9,13 @@ fn group_by_generates_group_by_sql() {
let source = users::table.group_by(users::name).select(users::id).filter(users::hair_color.is_null());
let mut expected_sql = "SELECT `users`.`id` FROM `users` \
WHERE `users`.`hair_color` IS NULL \
GROUP BY `users`.`name`".to_string();
GROUP BY `users`.`name` \
-- binds: []".to_string();
if cfg!(feature = "postgres") {
expected_sql = expected_sql.replace('`', "\"");
}
assert_eq!(expected_sql, debug_sql::<TestBackend, _>(&source));
assert_eq!(expected_sql, debug_query::<TestBackend, _>(&source).to_string());
}
#[test]
@ -28,10 +29,11 @@ fn boxed_queries_have_group_by_method() {
.filter(users::hair_color.is_null());
let mut expected_sql = "SELECT `users`.`id` FROM `users` \
WHERE `users`.`hair_color` IS NULL \
GROUP BY `users`.`name`".to_string();
GROUP BY `users`.`name` \
-- binds: []".to_string();
if cfg!(feature = "postgres") {
expected_sql = expected_sql.replace('`', "\"");
}
assert_eq!(expected_sql, debug_sql(&source));
assert_eq!(expected_sql, debug_query(&source).to_string());
}

View File

@ -4,6 +4,10 @@
use std::time::SystemTime;
use diesel::prelude::*;
#[cfg(test)]
use diesel::debug_query;
#[cfg(test)]
use diesel::pg::Pg;
table! {
posts {
@ -38,8 +42,8 @@ fn examine_sql_from_publish_all_posts() {
use posts::dsl::*;
assert_eq!(
"UPDATE \"posts\" SET \"draft\" = $1".to_string(),
debug_sql!(diesel::update(posts).set(draft.eq(false)))
"UPDATE \"posts\" SET \"draft\" = $1 -- binds: [false]",
debug_query(&diesel::update(posts).set(draft.eq(false))).to_string()
);
}
@ -60,8 +64,9 @@ fn examine_sql_from_publish_pending_posts() {
let target = posts.filter(publish_at.lt(now));
assert_eq!(
"UPDATE \"posts\" SET \"draft\" = $1 \
WHERE \"posts\".\"publish_at\" < CURRENT_TIMESTAMP".to_string(),
debug_sql!(diesel::update(target).set(draft.eq(false)))
WHERE \"posts\".\"publish_at\" < CURRENT_TIMESTAMP \
-- binds: [false]",
debug_query(&diesel::update(target).set(draft.eq(false))).to_string()
);
}
@ -81,8 +86,9 @@ fn examine_sql_from_publish_post() {
visit_count: 0,
};
assert_eq!(
"UPDATE \"posts\" SET \"draft\" = $1 WHERE \"posts\".\"id\" = $2".to_string(),
debug_sql!(diesel::update(&post).set(posts::draft.eq(false)))
"UPDATE \"posts\" SET \"draft\" = $1 WHERE \"posts\".\"id\" = $2 \
-- binds: [false, 1]",
debug_query(&diesel::update(&post).set(posts::draft.eq(false))).to_string()
);
}
@ -98,8 +104,9 @@ fn examine_sql_from_increment_visit_counts() {
use posts::dsl::*;
assert_eq!(
"UPDATE \"posts\" SET \"visit_count\" = \"posts\".\"visit_count\" + $1".to_string(),
debug_sql!(diesel::update(posts).set(visit_count.eq(visit_count + 1)))
"UPDATE \"posts\" SET \"visit_count\" = \"posts\".\"visit_count\" + $1 \
-- binds: [1]",
debug_query::<Pg, _>(&diesel::update(posts).set(visit_count.eq(visit_count + 1))).to_string()
);
}
@ -123,8 +130,9 @@ fn examine_sql_from_hide_everything() {
body.eq("This post has been classified"),
));
assert_eq!(
"UPDATE \"posts\" SET \"title\" = $1, \"body\" = $2".to_string(),
debug_sql!(query)
"UPDATE \"posts\" SET \"title\" = $1, \"body\" = $2 \
-- binds: [\"[REDACTED]\", \"This post has been classified\"]",
debug_query::<Pg, _>(&query).to_string()
);
}
@ -135,22 +143,32 @@ pub fn update_from_post_fields(post: Post, conn: &PgConnection) -> QueryResult<u
#[test]
fn examine_sql_from_update_post_fields() {
let now = SystemTime::now();
let post = Post {
id: 1,
title: "".into(),
body: "".into(),
draft: false,
publish_at: SystemTime::now(),
publish_at: now,
visit_count: 0,
};
assert_eq!(
let sql = format!(
"UPDATE \"posts\" SET \
\"title\" = $1, \
\"body\" = $2, \
\"draft\" = $3, \
\"publish_at\" = $4, \
\"visit_count\" = $5".to_string(),
debug_sql!(diesel::update(posts::table).set(&post))
\"visit_count\" = $5 \
-- binds: [\
\"\", \
\"\", \
false, \
{:?}, \
0\
]", now);
assert_eq!(
sql,
debug_query(&diesel::update(posts::table).set(&post)).to_string()
);
}
@ -186,7 +204,8 @@ fn examine_sql_from_update_with_option() {
let query = diesel::update(posts::table)
.set(&post_form);
assert_eq!(
"UPDATE \"posts\" SET \"body\" = $1".to_string(),
debug_sql!(query)
"UPDATE \"posts\" SET \"body\" = $1 \
-- binds: [\"My new post\"]",
debug_query::<Pg, _>(&query).to_string()
);
}