Skip to content

Commit 34148e2

Browse files
committed
Allow (some) subselects to appear in an expression context
A subselect can be used as an expression by wrapping it in parenthesis as long as the query returns exactly one column, and exactly zero or one rows. The first constraint is trivial for us to enforce, but the only way we can enforce the second is by automatically adding `LIMIT 1` to the query when this method is called. We also need to make the SQL type nullable, for the same reasons as max, min, avg, etc. They all return null if the query returns zero rows. If disjointness on associated types ever lands, I suspect we can just implement `AsExpression` directly, and deprecate this method. That said, given that the SQL type has to become nullable, it may be better to keep the explicit method, so that we don't get questions on why `id.eq(subselect)` isn't working. Fixes diesel-rs#1308.
1 parent 270f3cf commit 34148e2

4 files changed

Lines changed: 89 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
5252
`Insertable` (meaning one field can map to more than one column). Add
5353
`#[diesel(embed)]` to the field to enable this behavior.
5454

55+
* Queries that treat a subselect as a single value (e.g. `foo = (subselect)`)
56+
are now supported by calling [`.single_value()`].
57+
58+
[`.single_value()`]: http://docs.diesel.rs/diesel/query_dsl/trait.QueryDsl.html#method.single_value
59+
5560
### Changed
5661

5762
* The bounds on `impl ToSql for Cow<'a, T>` have been loosened to no longer

diesel/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ pub mod helper_types {
255255
/// Represents the return type of `.distinct_on(expr)`
256256
#[cfg(feature = "postgres")]
257257
pub type DistinctOn<Source, Expr> = <Source as DistinctOnDsl<Expr>>::Output;
258+
259+
/// Represents the return type of `.single_value()`
260+
pub type SingleValue<Source> = <Source as SingleValueDsl>::Output;
258261
}
259262

260263
pub mod prelude {

diesel/src/query_dsl/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub mod select_dsl;
3737
#[doc(hidden)]
3838
pub mod filter_dsl;
3939
mod save_changes_dsl;
40+
mod single_value_dsl;
4041
mod offset_dsl;
4142
mod order_dsl;
4243

@@ -65,6 +66,7 @@ pub mod methods {
6566
pub use super::offset_dsl::OffsetDsl;
6667
pub use super::order_dsl::{OrderDsl, ThenOrderDsl};
6768
pub use super::select_dsl::SelectDsl;
69+
pub use super::single_value_dsl::SingleValueDsl;
6870
}
6971

7072
/// Methods used to construct select statements.
@@ -867,6 +869,51 @@ pub trait QueryDsl: Sized {
867869
{
868870
methods::BoxedDsl::internal_into_boxed(self)
869871
}
872+
873+
/// Wraps this select statement in parenthesis, allowing it to be used
874+
/// as an expression.
875+
///
876+
/// SQL allows queries such as `foo = (SELECT ...)`, as long as the
877+
/// subselect returns only a single column, and 0 or 1 rows. This method
878+
/// indicates that you expect the query to only return a single value (this
879+
/// will be enforced by adding `LIMIT 1`).
880+
///
881+
/// The SQL type of this will always be `Nullable`, as the query returns
882+
/// `NULL` if the table is empty or it otherwise returns 0 rows.
883+
///
884+
/// # Example
885+
///
886+
/// ```rust
887+
/// # #[macro_use] extern crate diesel;
888+
/// # include!("../doctest_setup.rs");
889+
/// #
890+
/// # fn main() {
891+
/// # run_test();
892+
/// # }
893+
/// #
894+
/// # fn run_test() -> QueryResult<()> {
895+
/// # use diesel::insert_into;
896+
/// # use schema::users::dsl::*;
897+
/// # use schema::posts;
898+
/// # let connection = establish_connection();
899+
/// insert_into(posts::table)
900+
/// .values(posts::user_id.eq(1))
901+
/// .execute(&connection)?;
902+
/// let last_post = posts::table
903+
/// .order(posts::id.desc());
904+
/// let most_recently_active_user = users.select(name)
905+
/// .filter(id.nullable().eq(last_post.select(posts::user_id).single_value()))
906+
/// .first::<String>(&connection)?;
907+
/// assert_eq!("Sean", most_recently_active_user);
908+
/// # Ok(())
909+
/// # }
910+
/// ```
911+
fn single_value(self) -> SingleValue<Self>
912+
where
913+
Self: methods::SingleValueDsl,
914+
{
915+
methods::SingleValueDsl::single_value(self)
916+
}
870917
}
871918

872919
impl<T: Table> QueryDsl for T {}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use dsl::Limit;
2+
use expression::grouped::Grouped;
3+
use expression::subselect::Subselect;
4+
use query_builder::SelectQuery;
5+
use sql_types::{IntoNullable, SingleValue};
6+
use super::methods::LimitDsl;
7+
8+
/// The `single_value` method
9+
///
10+
/// This trait should not be relied on directly by most apps. Its behavior is
11+
/// provided by [`QueryDsl`]. However, you may need a where clause on this trait
12+
/// to call `single_value` from generic code.
13+
///
14+
/// [`QueryDsl`]: ../trait.QueryDsl.html
15+
pub trait SingleValueDsl {
16+
/// The type returned by `.single_value`.
17+
type Output;
18+
19+
/// See the trait documentation.
20+
fn single_value(self) -> Self::Output;
21+
}
22+
23+
impl<T, ST> SingleValueDsl for T
24+
where
25+
Self: SelectQuery<SqlType = ST> + LimitDsl,
26+
ST: IntoNullable,
27+
ST::Nullable: SingleValue,
28+
{
29+
type Output = Grouped<Subselect<Limit<Self>, ST::Nullable>>;
30+
31+
fn single_value(self) -> Self::Output {
32+
Grouped(Subselect::new(self.limit(1)))
33+
}
34+
}

0 commit comments

Comments
 (0)