Skip to content

Commit

Permalink
feat: use exists for relational filters (#5104)
Browse files Browse the repository at this point in the history
* feat: use exists for relational filters

* fix: correct the inverted condition

* fix: move alias counting to Context

* fix: fix lints

* fix: change tests to ensure ordering

* fix: change more tests to ensure ordering

* fix: change more tests to ensure ordering

* doc: comments

* [integration]
  • Loading branch information
jacek-prisma authored Jan 16, 2025
1 parent 1635fdd commit c164b5d
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 232 deletions.
6 changes: 5 additions & 1 deletion quaint/src/ast/compare.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::ExpressionKind;
use super::{ExpressionKind, SelectQuery};
use crate::ast::{Column, ConditionTree, Expression};
use std::borrow::Cow;

Expand Down Expand Up @@ -46,6 +46,10 @@ pub enum Compare<'a> {
Any(Box<Expression<'a>>),
/// ALL (`left`)
All(Box<Expression<'a>>),
/// EXISTS (`query`)
Exists(Box<SelectQuery<'a>>),
/// NOT EXISTS (`query`)
NotExists(Box<SelectQuery<'a>>),
}

#[derive(Debug, Clone, PartialEq)]
Expand Down
8 changes: 8 additions & 0 deletions quaint/src/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,14 @@ pub trait Visitor<'a> {
self.write("ALL")?;
self.surround_with("(", ")", |s| s.visit_expression(*left))
}
Compare::Exists(query) => {
self.write("EXISTS")?;
self.surround_with("(", ")", |s| s.visit_sub_selection(*query))
}
Compare::NotExists(query) => {
self.write("NOT EXISTS")?;
self.surround_with("(", ")", |s| s.visit_sub_selection(*query))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,13 @@ mod ext_rel_filters {
test_data(&runner).await?;

insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { every: { Title: { contains: "Album" }}}}) { Name }}"#),
@r###"{"data":{"findManyArtist":[{"Name":"CompleteArtist"},{"Name":"ArtistWithoutAlbums"},{"Name":"ArtistWithOneAlbumWithoutTracks"},{"Name":"CompleteArtist2"},{"Name":"CompleteArtistWith2Albums"}]}}"###
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { every: { Title: { contains: "Album" }}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithOneAlbumWithoutTracks"},{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist"},{"Name":"CompleteArtist2"},{"Name":"CompleteArtistWith2Albums"}]}}"###
);

insta::assert_snapshot!(
Expand Down Expand Up @@ -228,8 +233,13 @@ mod ext_rel_filters {

// every|some
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { every: { Tracks: { some: { Bytes: { lt: 1000 }}}}}}) { Name }}"#),
@r###"{"data":{"findManyArtist":[{"Name":"CompleteArtist"},{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist2"},{"Name":"CompleteArtistWith2Albums"}]}}"###
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { every: { Tracks: { some: { Bytes: { lt: 1000 }}}}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist"},{"Name":"CompleteArtist2"},{"Name":"CompleteArtistWith2Albums"}]}}"###
);

insta::assert_snapshot!(
Expand All @@ -244,8 +254,13 @@ mod ext_rel_filters {
);

insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { every: { Tracks: { every: { TrackId: { in: [4,5,6,7] }}}}}}) { Name }}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithoutAlbums"},{"Name":"ArtistWithOneAlbumWithoutTracks"},{"Name":"CompleteArtistWith2Albums"}]}}"###
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { every: { Tracks: { every: { TrackId: { in: [4,5,6,7] }}}}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithOneAlbumWithoutTracks"},{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtistWith2Albums"}]}}"###
);

// every|none
Expand All @@ -261,8 +276,13 @@ mod ext_rel_filters {

// none|some
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { none: { Tracks: { some: { UnitPrice: { lt: 1 }}}}}}) { Name }}"#),
@r###"{"data":{"findManyArtist":[{"Name":"CompleteArtist"},{"Name":"ArtistWithoutAlbums"},{"Name":"ArtistWithOneAlbumWithoutTracks"},{"Name":"CompleteArtist2"}]}}"###
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { none: { Tracks: { some: { UnitPrice: { lt: 1 }}}}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithOneAlbumWithoutTracks"},{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist"},{"Name":"CompleteArtist2"}]}}"###
);

insta::assert_snapshot!(
Expand All @@ -272,8 +292,13 @@ mod ext_rel_filters {

// none|every
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { none: { Tracks: { every: { UnitPrice: { gte: 5 }}}}}}) { Name }}"#),
@r###"{"data":{"findManyArtist":[{"Name":"CompleteArtist"},{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist2"}]}}"###
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { none: { Tracks: { every: { UnitPrice: { gte: 5 }}}}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist"},{"Name":"CompleteArtist2"}]}}"###
);

insta::assert_snapshot!(
Expand All @@ -283,13 +308,23 @@ mod ext_rel_filters {

// none|none
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { none: { Tracks: { none: { Bytes: { lt: 100 }}}}}}) { Name }}"#),
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { none: { Tracks: { none: { Bytes: { lt: 100 }}}}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist2"}]}}"###
);

insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyArtist(where: { Albums: { none: { Tracks: { none: { Bytes: { gte: 100 }}}}}}) { Name }}"#),
@r###"{"data":{"findManyArtist":[{"Name":"CompleteArtist"},{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist2"},{"Name":"CompleteArtistWith2Albums"}]}}"###
run_query!(&runner, r#"{
findManyArtist(
where: { Albums: { none: { Tracks: { none: { Bytes: { gte: 100 }}}}}}
orderBy: { Name: asc }
) { Name }
}"#),
@r###"{"data":{"findManyArtist":[{"Name":"ArtistWithoutAlbums"},{"Name":"CompleteArtist"},{"Name":"CompleteArtist2"},{"Name":"CompleteArtistWith2Albums"}]}}"###
);

Ok(())
Expand All @@ -311,8 +346,13 @@ mod ext_rel_filters {
async fn rel_filter_l2_implicit_and_every(runner: Runner) -> TestResult<()> {
test_data(&runner).await?;
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyAlbum(where: { Tracks: { every: { MediaType: { is: { Name: { equals: "MediaType1" }}}, Genre: { is: { Name: { equals: "Genre1" }}}}}}) { Title }}"#),
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"TheAlbumWithoutTracks"},{"Title":"Album4"}]}}"###
run_query!(&runner, r#"{
findManyAlbum(
where: { Tracks: { every: { MediaType: { is: { Name: { equals: "MediaType1" }}}, Genre: { is: { Name: { equals: "Genre1" }}}}}}
orderBy: { Title: asc }
) { Title }
}"#),
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"Album4"},{"Title":"TheAlbumWithoutTracks"}]}}"###
);

Ok(())
Expand Down Expand Up @@ -345,8 +385,13 @@ mod ext_rel_filters {
test_data(&runner).await?;

insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyAlbum(where: { Tracks: { every: { AND: [{ MediaType: { is: { Name: { equals: "MediaType1" }}}}, { Genre: { is: { Name: { equals: "Genre1" }}}}]}}}) { Title }}"#),
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"TheAlbumWithoutTracks"},{"Title":"Album4"}]}}"###
run_query!(&runner, r#"{
findManyAlbum(
where: { Tracks: { every: { AND: [{ MediaType: { is: { Name: { equals: "MediaType1" }}}}, { Genre: { is: { Name: { equals: "Genre1" }}}}]}}}
orderBy: { Title: asc }
) { Title }
}"#),
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"Album4"},{"Title":"TheAlbumWithoutTracks"}]}}"###
);

insta::assert_snapshot!(
Expand All @@ -373,8 +418,12 @@ mod ext_rel_filters {
);

insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyAlbum(where: { Tracks: { every: {OR:[{ MediaType: { is: { Name: { equals: "MediaType1"}}}},{Genre: { is: { Name: { equals: "Genre2"}}}}]}}}) { Title }}"#),
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"TheAlbumWithoutTracks"},{"Title":"Album4"},{"Title":"Album5"}]}}"###
run_query!(&runner, r#"{
findManyAlbum(
where: { Tracks: { every: {OR:[{ MediaType: { is: { Name: { equals: "MediaType1"}}}},{Genre: { is: { Name: { equals: "Genre2"}}}}]}}}
orderBy: { Title: asc }
) { Title }}"#),
@r###"{"data":{"findManyAlbum":[{"Title":"Album1"},{"Title":"Album4"},{"Title":"Album5"},{"Title":"TheAlbumWithoutTracks"}]}}"###
);

insta::assert_snapshot!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,13 @@ mod fr_m_to_n {
.await?;

insta::assert_snapshot!(
run_query!(&runner, r#"query { findManyCompany(where: { locations: { none: { name: { equals: "D" }}}}){ id }}"#),
run_query!(&runner, r#"
query {
findManyCompany(
where: { locations: { none: { name: { equals: "D" }}}}
orderBy: { id: asc }
) { id }
}"#),
@r###"{"data":{"findManyCompany":[{"id":134},{"id":135},{"id":136}]}}"###
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,13 @@ mod many_relation {

// none / is
insta::assert_snapshot!(
run_query!(&runner, r#"query { findManyBlog(where: { posts: { none: { comment: { is: { popularity: { lt: 1000 } } } } } }) { name }}"#),
run_query!(&runner, r#"
query {
findManyBlog(
where: { posts: { none: { comment: { is: { popularity: { lt: 1000 } } } } } }
orderBy: { name: asc }
) { name }
}"#),
@r###"{"data":{"findManyBlog":[{"name":"blog2"},{"name":"blog3"},{"name":"blog4"}]}}"###
);

Expand All @@ -404,7 +410,13 @@ mod many_relation {
// TODO: Investigate why MongoDB returns a different result
match_connector_result!(
&runner,
r#"query { findManyBlog(where: { posts: { every: { comment: { isNot: { popularity: { gte: 1000 } } } } } }) { name }}"#,
r#"
query {
findManyBlog(
where: { posts: { every: { comment: { isNot: { popularity: { gte: 1000 } } } } } }
orderBy: { name: asc }
) { name }
}"#,
MongoDb(_) => vec![r#"{"data":{"findManyBlog":[{"name":"blog1"},{"name":"blog4"}]}}"#],
_ => vec![r#"{"data":{"findManyBlog":[{"name":"blog1"},{"name":"blog3"},{"name":"blog4"}]}}"#]
);
Expand Down
4 changes: 4 additions & 0 deletions query-engine/connector-test-kit-rs/test-configs/postgres16
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"connector": "postgres",
"version": "16"
}
16 changes: 16 additions & 0 deletions query-engine/query-builders/sql-query-builder/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use std::sync::{self, atomic::AtomicUsize};

use quaint::prelude::ConnectionInfo;
use telemetry::TraceParent;

use crate::filter::alias::Alias;

pub struct Context<'a> {
connection_info: &'a ConnectionInfo,
pub(crate) traceparent: Option<TraceParent>,
Expand All @@ -10,6 +14,8 @@ pub struct Context<'a> {
/// Maximum number of bind parameters allowed for a single query.
/// None is unlimited.
pub(crate) max_bind_values: Option<usize>,

alias_counter: AtomicUsize,
}

impl<'a> Context<'a> {
Expand All @@ -22,6 +28,8 @@ impl<'a> Context<'a> {
traceparent,
max_insert_rows,
max_bind_values: Some(max_bind_values),

alias_counter: Default::default(),
}
}

Expand All @@ -40,4 +48,12 @@ impl<'a> Context<'a> {
pub fn max_bind_values(&self) -> Option<usize> {
self.max_bind_values
}

pub(crate) fn next_table_alias(&self) -> Alias {
Alias::Table(self.alias_counter.fetch_add(1, sync::atomic::Ordering::SeqCst))
}

pub(crate) fn next_join_alias(&self) -> Alias {
Alias::Join(self.alias_counter.fetch_add(1, sync::atomic::Ordering::SeqCst))
}
}
63 changes: 22 additions & 41 deletions query-engine/query-builders/sql-query-builder/src/filter/alias.rs
Original file line number Diff line number Diff line change
@@ -1,59 +1,40 @@
use std::fmt;

use crate::{model_extensions::AsColumn, *};

use quaint::prelude::Column;
use query_structure::ScalarField;

#[derive(Clone, Copy, Debug)]
/// A distinction in aliasing to separate the parent table and the joined data
/// in the statement.
#[derive(Default)]
pub enum AliasMode {
#[default]
Table,
Join,
}

#[derive(Clone, Copy, Debug, Default)]
/// Aliasing tool to count the nesting level to help with heavily nested
/// self-related queries.
pub struct Alias {
counter: usize,
mode: AliasMode,
/// An alias referring to a table or a join on a table.
#[derive(Debug, Clone, Copy)]
pub enum Alias {
Table(usize),
Join(usize),
}

impl Alias {
/// Increment the alias as a new copy.
///
/// Use when nesting one level down to a new subquery. `AliasMode` is
/// required due to the fact the current mode can be in `AliasMode::Join`.
pub fn inc(&self, mode: AliasMode) -> Self {
Self {
counter: self.counter + 1,
mode,
/// Converts the alias to one that refers to a join on the table.
pub fn to_join_alias(self) -> Self {
match self {
Self::Table(index) | Self::Join(index) => Self::Join(index),
}
}

/// Flip the alias to a different mode keeping the same nesting count.
pub fn flip(&self, mode: AliasMode) -> Self {
Self {
counter: self.counter,
mode,
/// Converts the alias to one that refers to the table.
pub fn to_table_alias(self) -> Self {
match self {
Self::Table(index) | Self::Join(index) => Self::Table(index),
}
}
}

/// A string representation of the current alias. The current mode can be
/// overridden by defining the `mode_override`.
pub fn to_string(self, mode_override: Option<AliasMode>) -> String {
match mode_override.unwrap_or(self.mode) {
AliasMode::Table => format!("t{}", self.counter),
AliasMode::Join => format!("j{}", self.counter),
impl fmt::Display for Alias {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Table(index) => write!(f, "t{}", index),
Self::Join(index) => write!(f, "j{}", index),
}
}

#[cfg(feature = "relation_joins")]
pub fn to_table_string(self) -> String {
self.to_string(Some(AliasMode::Table))
}
}

pub(crate) trait AliasedColumn {
Expand All @@ -73,7 +54,7 @@ impl AliasedColumn for &ScalarField {
impl AliasedColumn for Column<'static> {
fn aliased_col(self, alias: Option<Alias>, _ctx: &Context<'_>) -> Column<'static> {
match alias {
Some(alias) => self.table(alias.to_string(None)),
Some(alias) => self.table(alias.to_string()),
None => self,
}
}
Expand Down
Loading

0 comments on commit c164b5d

Please sign in to comment.