Skip to content

Commit

Permalink
feat: resolve relations with lateral joins (#4509)
Browse files Browse the repository at this point in the history
  • Loading branch information
Weakky committed Dec 5, 2023
1 parent a26fa4c commit 1964a5c
Show file tree
Hide file tree
Showing 80 changed files with 2,307 additions and 219 deletions.
2 changes: 1 addition & 1 deletion .envrc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export SIMPLE_TEST_MODE="yes" # Reduces the amount of generated `relation_link_t
### QE specific logging vars ###
export QE_LOG_LEVEL=debug # Set it to "trace" to enable query-graph debugging logs
# export PRISMA_RENDER_DOT_FILE=1 # Uncomment to enable rendering a dot file of the Query Graph from an executed query.
# export FMT_SQL=1 # Uncomment it to enable logging formatted SQL queries
export FMT_SQL=1 # Uncomment it to enable logging formatted SQL queries

### Uncomment to run driver adapters tests. See query-engine-driver-adapters.yml workflow for how tests run in CI.
# export EXTERNAL_TEST_EXECUTOR="napi"
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions libs/prisma-value/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ impl PrismaValue {
_ => None,
}
}

pub fn as_json(&self) -> Option<&String> {
if let Self::Json(v) = self {
Some(v)
} else {
None
}
}
}

impl fmt::Display for PrismaValue {
Expand Down
3 changes: 2 additions & 1 deletion psl/builtin-connectors/src/cockroach_datamodel_connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ const CAPABILITIES: ConnectorCapabilities = enumflags2::make_bitflags!(Connector
FilteredInlineChildNestedToOneDisconnect |
InsertReturning |
UpdateReturning |
RowIn
RowIn |
LateralJoin
});

const SCALAR_TYPE_DEFAULTS: &[(ScalarType, CockroachType)] = &[
Expand Down
3 changes: 2 additions & 1 deletion psl/builtin-connectors/src/postgres_datamodel_connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const CAPABILITIES: ConnectorCapabilities = enumflags2::make_bitflags!(Connector
InsertReturning |
UpdateReturning |
RowIn |
DistinctOn
DistinctOn |
LateralJoin
});

pub struct PostgresDatamodelConnector;
Expand Down
2 changes: 2 additions & 0 deletions psl/psl-core/src/common/preview_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ features!(
TransactionApi,
UncheckedScalarInputs,
Views,
RelationJoins
);

/// Generator preview features (alphabetically sorted)
Expand All @@ -92,6 +93,7 @@ pub const ALL_PREVIEW_FEATURES: FeatureMap = FeatureMap {
| PostgresqlExtensions
| Tracing
| Views
| RelationJoins
}),
deprecated: enumflags2::make_bitflags!(PreviewFeature::{
AtomicNumberOperations
Expand Down
3 changes: 2 additions & 1 deletion psl/psl-core/src/datamodel_connector/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ capabilities!(
InsertReturning,
UpdateReturning,
RowIn, // Connector supports (a, b) IN (c, d) expression.
DistinctOn // Connector supports DB-level distinct (e.g. postgres)
DistinctOn, // Connector supports DB-level distinct (e.g. postgres)
LateralJoin,
);

/// Contains all capabilities that the connector is able to serve.
Expand Down
2 changes: 1 addition & 1 deletion psl/psl/tests/config/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ fn nice_error_for_unknown_generator_preview_feature() {
.unwrap_err();

let expectation = expect![[r#"
[1;91merror[0m: [1mThe preview feature "foo" is not known. Expected one of: deno, driverAdapters, fullTextIndex, fullTextSearch, metrics, multiSchema, nativeDistinct, postgresqlExtensions, tracing, views[0m
[1;91merror[0m: [1mThe preview feature "foo" is not known. Expected one of: distinctOn, deno, driverAdapters, fullTextIndex, fullTextSearch, metrics, multiSchema, postgresqlExtensions, tracing, views, relationJoins[0m
--> schema.prisma:3
 | 
 2 |  provider = "prisma-client-js"
Expand Down
10 changes: 9 additions & 1 deletion quaint/src/ast/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ mod average;
mod coalesce;
mod concat;
mod count;
mod json_array_agg;
mod json_build_obj;
#[cfg(any(feature = "postgresql", feature = "mysql"))]
mod json_extract;
#[cfg(any(feature = "postgresql", feature = "mysql"))]
Expand All @@ -28,6 +30,8 @@ pub use average::*;
pub use coalesce::*;
pub use concat::*;
pub use count::*;
pub use json_array_agg::*;
pub use json_build_obj::*;
#[cfg(any(feature = "postgresql", feature = "mysql"))]
pub use json_extract::*;
#[cfg(any(feature = "postgresql", feature = "mysql"))]
Expand Down Expand Up @@ -98,6 +102,8 @@ pub(crate) enum FunctionType<'a> {
JsonExtractFirstArrayElem(JsonExtractFirstArrayElem<'a>),
#[cfg(any(feature = "postgresql", feature = "mysql"))]
JsonUnquote(JsonUnquote<'a>),
JsonArrayAgg(JsonArrayAgg<'a>),
JsonBuildObject(JsonBuildObject<'a>),
#[cfg(any(feature = "postgresql", feature = "mysql"))]
TextSearch(TextSearch<'a>),
#[cfg(any(feature = "postgresql", feature = "mysql"))]
Expand Down Expand Up @@ -154,5 +160,7 @@ function!(
Minimum,
Maximum,
Coalesce,
Concat
Concat,
JsonArrayAgg,
JsonBuildObject
);
18 changes: 18 additions & 0 deletions quaint/src/ast/function/json_array_agg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use crate::prelude::*;

#[derive(Debug, Clone, PartialEq)]
pub struct JsonArrayAgg<'a> {
pub(crate) expr: Box<Expression<'a>>,
}

/// Builds a JSON array out of a list of values.
pub fn json_array_agg<'a, E>(expr: E) -> Function<'a>
where
E: Into<Expression<'a>>,
{
let fun = JsonArrayAgg {
expr: Box::new(expr.into()),
};

fun.into()
}
15 changes: 15 additions & 0 deletions quaint/src/ast/function/json_build_obj.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use std::borrow::Cow;

use crate::prelude::*;

#[derive(Debug, Clone, PartialEq)]
pub struct JsonBuildObject<'a> {
pub(crate) exprs: Vec<(Cow<'a, str>, Expression<'a>)>,
}

/// Builds a JSON object out of a list of key-value pairs.
pub fn json_build_object<'a>(exprs: Vec<(Cow<'a, str>, Expression<'a>)>) -> Function<'a> {
let fun = JsonBuildObject { exprs };

fun.into()
}
9 changes: 9 additions & 0 deletions quaint/src/ast/join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::ast::{ConditionTree, Table};
pub struct JoinData<'a> {
pub(crate) table: Table<'a>,
pub(crate) conditions: ConditionTree<'a>,
pub(crate) lateral: bool,
}

impl<'a> JoinData<'a> {
Expand All @@ -13,8 +14,14 @@ impl<'a> JoinData<'a> {
Self {
table: table.into(),
conditions: ConditionTree::NoCondition,
lateral: false,
}
}

pub fn lateral(mut self) -> Self {
self.lateral = true;
self
}
}

impl<'a, T> From<T> for JoinData<'a>
Expand Down Expand Up @@ -73,6 +80,7 @@ where
JoinData {
table: self.into(),
conditions: conditions.into(),
lateral: false,
}
}
}
Expand All @@ -90,6 +98,7 @@ impl<'a> Joinable<'a> for JoinData<'a> {
JoinData {
table: self.table,
conditions,
lateral: false,
}
}
}
9 changes: 9 additions & 0 deletions quaint/src/ast/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,15 @@ impl<'a> Select<'a> {
self
}

pub fn left_join_lateral<J>(self, join: J) -> Self
where
J: Into<JoinData<'a>>,
{
let join_data: JoinData = join.into();

self.left_join(join_data.lateral())
}

/// Adds `RIGHT JOIN` clause to the query.
///
/// ```rust
Expand Down
9 changes: 9 additions & 0 deletions quaint/src/ast/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ impl<'a> Table<'a> {
self
}

pub fn left_join_lateral<J>(self, join: J) -> Self
where
J: Into<JoinData<'a>>,
{
let join_data: JoinData = join.into();

self.left_join(join_data.lateral())
}

/// Adds an `INNER JOIN` clause to the query, specifically for that table.
/// Useful to positionally add a JOIN clause in case you are selecting from multiple tables.
///
Expand Down
41 changes: 41 additions & 0 deletions quaint/src/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,38 @@ pub trait Visitor<'a> {
match j {
Join::Inner(data) => {
self.write(" INNER JOIN ")?;

if data.lateral {
self.write("LATERAL ")?;
}

self.visit_join_data(data)?;
}
Join::Left(data) => {
self.write(" LEFT JOIN ")?;

if data.lateral {
self.write("LATERAL ")?;
}

self.visit_join_data(data)?;
}
Join::Right(data) => {
self.write(" RIGHT JOIN ")?;

if data.lateral {
self.write("LATERAL ")?;
}

self.visit_join_data(data)?;
}
Join::Full(data) => {
self.write(" FULL JOIN ")?;

if data.lateral {
self.write("LATERAL ")?;
}

self.visit_join_data(data)?;
}
}
Expand Down Expand Up @@ -1112,6 +1132,27 @@ pub trait Visitor<'a> {
FunctionType::Concat(concat) => {
self.visit_concat(concat)?;
}
FunctionType::JsonArrayAgg(array_agg) => {
self.write("JSON_AGG")?;
self.surround_with("(", ")", |s| s.visit_expression(*array_agg.expr))?;
}
FunctionType::JsonBuildObject(build_obj) => {
let len = build_obj.exprs.len();

self.write("JSON_BUILD_OBJECT")?;
self.surround_with("(", ")", |s| {
for (i, (name, expr)) in build_obj.exprs.into_iter().enumerate() {
s.visit_raw_value(Value::text(name))?;
s.write(", ")?;
s.visit_expression(expr)?;
if i < (len - 1) {
s.write(", ")?;
}
}

Ok(())
})?;
}
};

if let Some(alias) = fun.alias {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ mod multi_schema {
insta::assert_snapshot!(
run_query!(&runner, r#"
query {
findManyCategoriesOnPosts(where: {postId: {gt: 0}}) {
findManyCategoriesOnPosts(orderBy: [{ postId: asc }, { categoryId: asc }], where: {postId: {gt: 0}}) {
category {
name
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ mod not_in_batching {
assert_error!(
runner,
"query { findManyTestModel(where: { id: { notIn: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] } }) { id }}",
2029,
"Parameter limits for this database provider require this query to be split into multiple queries, but the negation filters used prevent the query from being split. Please reduce the used values in the query."
2029 // QueryParameterLimitExceeded
);

Ok(())
Expand All @@ -30,8 +29,7 @@ mod not_in_batching_cockroachdb {
assert_error!(
runner,
"query { findManyTestModel(where: { id: { notIn: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] } }) { id }}",
2029,
"Parameter limits for this database provider require this query to be split into multiple queries, but the negation filters used prevent the query from being split. Please reduce the used values in the query."
2029 // QueryParameterLimitExceeded
);

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ mod isb {
}

// "batching of IN queries" should "work when having more than the specified amount of items"
#[connector_test]
// TODO(joins): Excluded because we have no support for batched queries with joins. In practice, it should happen under much less circumstances
// TODO(joins): than with the query-based strategy, because we don't issue `WHERE IN (parent_ids)` queries anymore to resolve relations.
#[connector_test(exclude_features("relationJoins"))]
async fn in_more_items(runner: Runner) -> TestResult<()> {
create_test_data(&runner).await?;

Expand All @@ -51,7 +53,9 @@ mod isb {
}

// "ascending ordering of batched IN queries" should "work when having more than the specified amount of items"
#[connector_test]
// TODO(joins): Excluded because we have no support for batched queries with joins. In practice, it should happen under much less circumstances
// TODO(joins): than with the query-based strategy, because we don't issue `WHERE IN (parent_ids)` queries anymore to resolve relations.
#[connector_test(exclude_features("relationJoins"))]
async fn asc_in_ordering(runner: Runner) -> TestResult<()> {
create_test_data(&runner).await?;

Expand All @@ -67,7 +71,9 @@ mod isb {
}

// "ascending ordering of batched IN queries" should "work when having more than the specified amount of items"
#[connector_test]
// TODO(joins): Excluded because we have no support for batched queries with joins. In practice, it should happen under much less circumstances
// TODO(joins): than with the query-based strategy, because we don't issue `WHERE IN (parent_ids)` queries anymore to resolve relations.
#[connector_test(exclude_features("relationJoins"))]
async fn desc_in_ordering(runner: Runner) -> TestResult<()> {
create_test_data(&runner).await?;

Expand All @@ -91,8 +97,7 @@ mod isb {
r#"query {
findManyA(where: {id: { in: [5,4,3,2,1,1,1,2,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }}, orderBy: { b: { as: { _count: asc } } }) { id }
}"#,
2029,
"Your query cannot be split into multiple queries because of the order by aggregation or relevance."
2029 // QueryParameterLimitExceeded
);

Ok(())
Expand All @@ -107,8 +112,7 @@ mod isb {
r#"query {
findManyA(where: {id: { in: [5,4,3,2,1,1,1,2,3,4,5,6,7,6,5,4,3,2,1,2,3,4,5,6] }}, orderBy: { _relevance: { fields: text, search: "something", sort: asc } }) { id }
}"#,
2029,
"Your query cannot be split into multiple queries because of the order by aggregation or relevance."
2029 // QueryParameterLimitExceeded
);

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ mod float;
mod int;
mod json;
mod string;
mod through_relation;
Loading

0 comments on commit 1964a5c

Please sign in to comment.