From 4c0582df7b105ceb5355a4bf5432f6299462e5c0 Mon Sep 17 00:00:00 2001 From: Flavian Desverne Date: Mon, 27 Nov 2023 16:40:05 +0100 Subject: [PATCH] update join query builder to better support filters, ordering and pagination --- .../sql-query-connector/src/filter/alias.rs | 6 +- .../sql-query-connector/src/filter/mod.rs | 4 +- .../sql-query-connector/src/filter/visitor.rs | 7 +- .../sql-query-connector/src/ordering.rs | 14 +- .../src/query_builder/read.rs | 20 +- .../src/query_builder/select.rs | 500 ++++++++++-------- query-engine/core/src/response_ir/internal.rs | 6 +- 7 files changed, 319 insertions(+), 238 deletions(-) diff --git a/query-engine/connectors/sql-query-connector/src/filter/alias.rs b/query-engine/connectors/sql-query-connector/src/filter/alias.rs index c7a62bba02a..af3ad932748 100644 --- a/query-engine/connectors/sql-query-connector/src/filter/alias.rs +++ b/query-engine/connectors/sql-query-connector/src/filter/alias.rs @@ -16,7 +16,7 @@ pub enum AliasMode { #[derive(Clone, Copy, Debug, Default)] /// Aliasing tool to count the nesting level to help with heavily nested /// self-related queries. -pub(crate) struct Alias { +pub struct Alias { counter: usize, mode: AliasMode, } @@ -49,6 +49,10 @@ impl Alias { AliasMode::Join => format!("j{}", self.counter), } } + + pub fn to_table_string(&self) -> String { + self.to_string(Some(AliasMode::Table)) + } } pub(crate) trait AliasedColumn { diff --git a/query-engine/connectors/sql-query-connector/src/filter/mod.rs b/query-engine/connectors/sql-query-connector/src/filter/mod.rs index b9ae856ef65..573024845b4 100644 --- a/query-engine/connectors/sql-query-connector/src/filter/mod.rs +++ b/query-engine/connectors/sql-query-connector/src/filter/mod.rs @@ -1,9 +1,9 @@ -mod alias; +pub mod alias; mod visitor; use quaint::prelude::*; use query_structure::Filter; -use visitor::*; +pub use visitor::*; use crate::{context::Context, join_utils::AliasedJoin}; diff --git a/query-engine/connectors/sql-query-connector/src/filter/visitor.rs b/query-engine/connectors/sql-query-connector/src/filter/visitor.rs index 1a71cdd824a..b27ab539e60 100644 --- a/query-engine/connectors/sql-query-connector/src/filter/visitor.rs +++ b/query-engine/connectors/sql-query-connector/src/filter/visitor.rs @@ -27,7 +27,7 @@ pub(crate) trait FilterVisitorExt { } #[derive(Debug, Clone, Default)] -pub(crate) struct FilterVisitor { +pub struct FilterVisitor { /// The last alias that's been rendered. last_alias: Option, /// The parent alias, used when rendering nested filters so that a child filter can refer to its join. @@ -68,6 +68,11 @@ impl FilterVisitor { self.parent_alias } + pub fn set_parent_alias_opt(mut self, alias: Option) -> Self { + self.parent_alias = alias; + self + } + /// A top-level join can be rendered if we're explicitly allowing it or if we're in a nested visitor. fn can_render_join(&self) -> bool { self.with_top_level_joins || self.is_nested diff --git a/query-engine/connectors/sql-query-connector/src/ordering.rs b/query-engine/connectors/sql-query-connector/src/ordering.rs index 310e10ec43d..2332f28e643 100644 --- a/query-engine/connectors/sql-query-connector/src/ordering.rs +++ b/query-engine/connectors/sql-query-connector/src/ordering.rs @@ -150,9 +150,14 @@ impl OrderByBuilder { // Unwraps are safe because the SQL connector doesn't yet support any other type of orderBy hop but the relation hop. let mut joins: Vec = vec![]; + let parent_alias = self.parent_alias.clone(); + for (i, hop) in rest_hops.iter().enumerate() { let previous_join = if i > 0 { joins.get(i - 1) } else { None }; - let previous_alias = previous_join.map(|j| j.alias.as_str()); + + let previous_alias = previous_join + .map(|j| j.alias.as_str()) + .or_else(|| parent_alias.as_deref()); let join = compute_one2m_join(hop.as_relation_hop().unwrap(), &self.join_prefix(), previous_alias, ctx); joins.push(join); @@ -192,9 +197,14 @@ impl OrderByBuilder { ) -> (Vec, Column<'static>) { let mut joins: Vec = vec![]; + let parent_alias = self.parent_alias.clone(); + for (i, hop) in order_by.path.iter().enumerate() { let previous_join = if i > 0 { joins.get(i - 1) } else { None }; - let previous_alias = previous_join.map(|j| j.alias.as_str()); + let previous_alias = previous_join + .map(|j| &j.alias) + .or(parent_alias.as_ref()) + .map(|alias| alias.as_str()); let join = compute_one2m_join(hop.as_relation_hop().unwrap(), &self.join_prefix(), previous_alias, ctx); joins.push(join); diff --git a/query-engine/connectors/sql-query-connector/src/query_builder/read.rs b/query-engine/connectors/sql-query-connector/src/query_builder/read.rs index 6291ce0cd16..3aa91288ea9 100644 --- a/query-engine/connectors/sql-query-connector/src/query_builder/read.rs +++ b/query-engine/connectors/sql-query-connector/src/query_builder/read.rs @@ -2,19 +2,16 @@ use crate::{ cursor_condition, filter::FilterBuilder, model_extensions::*, nested_aggregations, ordering::OrderByBuilder, sql_trace::SqlTraceComment, Context, }; -use connector_interface::{AggregationSelection, RelAggregationSelection, RelatedQuery}; +use connector_interface::{AggregationSelection, RelAggregationSelection}; use itertools::Itertools; use quaint::ast::*; use query_structure::*; use tracing::Span; -use super::select; - pub(crate) trait SelectDefinition { fn into_select( self, _: &Model, - nested: Vec, aggr_selections: &[RelAggregationSelection], ctx: &Context<'_>, ) -> (Select<'static>, Vec>); @@ -24,12 +21,11 @@ impl SelectDefinition for Filter { fn into_select( self, model: &Model, - nested: Vec, aggr_selections: &[RelAggregationSelection], ctx: &Context<'_>, ) -> (Select<'static>, Vec>) { let args = QueryArguments::from((model.clone(), self)); - args.into_select(model, nested, aggr_selections, ctx) + args.into_select(model, aggr_selections, ctx) } } @@ -37,11 +33,10 @@ impl SelectDefinition for &Filter { fn into_select( self, model: &Model, - nested: Vec, aggr_selections: &[RelAggregationSelection], ctx: &Context<'_>, ) -> (Select<'static>, Vec>) { - self.clone().into_select(model, nested, aggr_selections, ctx) + self.clone().into_select(model, aggr_selections, ctx) } } @@ -49,7 +44,6 @@ impl SelectDefinition for Select<'static> { fn into_select( self, _: &Model, - _: Vec, _: &[RelAggregationSelection], _ctx: &Context<'_>, ) -> (Select<'static>, Vec>) { @@ -61,7 +55,6 @@ impl SelectDefinition for QueryArguments { fn into_select( self, model: &Model, - nested: Vec, aggr_selections: &[RelAggregationSelection], ctx: &Context<'_>, ) -> (Select<'static>, Vec>) { @@ -125,13 +118,12 @@ pub(crate) fn get_records( columns: impl Iterator>, aggr_selections: &[RelAggregationSelection], query: T, - nested: Vec, ctx: &Context<'_>, ) -> Select<'static> where T: SelectDefinition, { - let (select, additional_selection_set) = query.into_select(model, nested, aggr_selections, ctx); + let (select, additional_selection_set) = query.into_select(model, aggr_selections, ctx); let select = columns.fold(select, |acc, col| acc.column(col)); let select = select.append_trace(&Span::current()).add_trace_id(ctx.trace_id); @@ -174,7 +166,7 @@ pub(crate) fn aggregate( ctx: &Context<'_>, ) -> Select<'static> { let columns = extract_columns(model, selections, ctx); - let sub_query = get_records(model, columns.into_iter(), &[], args, Vec::new(), ctx); + let sub_query = get_records(model, columns.into_iter(), &[], args, ctx); let sub_table = Table::from(sub_query).alias("sub"); selections.iter().fold( @@ -231,7 +223,7 @@ pub(crate) fn group_by_aggregate( having: Option, ctx: &Context<'_>, ) -> Select<'static> { - let (base_query, _) = args.into_select(model, Vec::new(), &[], ctx); + let (base_query, _) = args.into_select(model, &[], ctx); let select_query = selections.iter().fold(base_query, |select, next_op| match next_op { AggregationSelection::Field(field) => select.column(field.as_column(ctx).set_is_selected(true)), diff --git a/query-engine/connectors/sql-query-connector/src/query_builder/select.rs b/query-engine/connectors/sql-query-connector/src/query_builder/select.rs index b5ccae5f730..2ca856edbbd 100644 --- a/query-engine/connectors/sql-query-connector/src/query_builder/select.rs +++ b/query-engine/connectors/sql-query-connector/src/query_builder/select.rs @@ -2,264 +2,322 @@ use std::borrow::Cow; use crate::{ context::Context, - filter::FilterBuilder, + filter::alias::{Alias, AliasMode}, model_extensions::{AsColumn, AsColumns, AsTable, RelationFieldExt}, ordering::OrderByBuilder, }; -use connector_interface::{Filter, QueryArguments, RelAggregationSelection, RelatedQuery}; use itertools::Itertools; -use prisma_models::{ModelProjection, RelationField, ScalarField}; use quaint::prelude::*; +use query_structure::*; pub const JSON_AGG_IDENT: &str = "data"; -pub(crate) fn build( - args: QueryArguments, - nested: Vec, - selection: &ModelProjection, - _aggr_selections: &[RelAggregationSelection], - ctx: &Context<'_>, -) -> Select<'static> { - // SELECT ... FROM Table - let select = Select::from_table(args.model().as_table(ctx)); - - // scalars selection - let select = selection - .scalar_fields() - .fold(select, |acc, sf| acc.column(sf.as_column(ctx))); - - // TODO: check how to select aggregated relations - // Adds relation selections to the top-level query - let select = nested.iter().fold(select, |acc, read| { - let table_name = match read.parent_field.relation().is_many_to_many() { - true => m2m_join_alias_name(&read.parent_field), - false => join_alias_name(&read.parent_field), - }; - - acc.value(Column::from((table_name, JSON_AGG_IDENT)).alias(read.name.to_owned())) - }); - - // Adds joins for relations - let select = with_related_queries(select, nested, ctx); - let select = with_ordering(select, &args, None, ctx); - let select = with_pagination(select, args.take, args.skip); - let select = with_filters(select, args.filter, ctx); - - select +#[derive(Debug, Default)] +pub(crate) struct SelectBuilder { + alias: Alias, } -fn with_related_queries<'a>(input: Select<'a>, related_queries: Vec, ctx: &Context<'_>) -> Select<'a> { - related_queries - .into_iter() - .fold(input, |acc, rq| with_related_query(acc, rq, ctx)) -} - -fn with_related_query<'a>(select: Select<'a>, rq: RelatedQuery, ctx: &Context<'_>) -> Select<'a> { - if rq.parent_field.relation().is_many_to_many() { - let m2m_join = build_m2m_join(rq, ctx); - - // m2m relations need to left join on the relation table first - select.left_join(m2m_join) - } else { - let alias = join_alias_name(&rq.parent_field); +impl SelectBuilder { + pub(crate) fn next_alias(&mut self) -> Alias { + self.alias = self.alias.inc(AliasMode::Table); + self.alias + } - // LEFT JOIN LATERAL () AS ON TRUE - let join_select = Table::from(build_related_query_select(rq, ctx)) - .alias(alias) - .on(ConditionTree::single(true.raw())) - .lateral(); + pub(crate) fn build( + &mut self, + args: QueryArguments, + selected_fields: &FieldSelection, + ctx: &Context<'_>, + ) -> Select<'static> { + let table_alias = self.next_alias(); + + // SELECT ... FROM Table "t1" + let select = Select::from_table(args.model().as_table(ctx).alias(table_alias.to_table_string())); + + // TODO: check how to select aggregated relations + let select = selected_fields + .selections() + .fold(select, |acc, selection| match selection { + SelectedField::Scalar(sf) => acc.column(sf.as_column(ctx).table(table_alias.to_table_string())), + SelectedField::Relation(rs) => { + let table_name = match rs.field.relation().is_many_to_many() { + true => m2m_join_alias_name(&rs.field), + false => join_alias_name(&rs.field), + }; + + acc.value(Column::from((table_name, JSON_AGG_IDENT)).alias(rs.field.name().to_owned())) + } + _ => acc, + }); + + // Adds joins for relations + let select = self.with_related_queries(select, selected_fields.relations(), table_alias, ctx); + let select = self.with_ordering(select, &args, Some(table_alias.to_table_string()), ctx); + let select = self.with_pagination(select, args.take_abs(), args.skip); + let select = self.with_filters(select, args.filter, Some(table_alias), ctx); - select.left_join(join_select) + select } -} -fn build_related_query_select(rq: RelatedQuery, ctx: &Context<'_>) -> Select<'static> { - let mut fields_to_select: Vec = vec![]; - - let mut build_obj_params = ModelProjection::from(rq.selected_fields) - .fields() - .map(|f| match f { - prisma_models::Field::Scalar(sf) => { - (Cow::from(sf.db_name().to_owned()), Expression::from(sf.as_column(ctx))) - } - _ => unreachable!(), - }) - .collect_vec(); - - if let Some(nested_queries) = &rq.nested { - for nested_query in nested_queries { - let table_name = match nested_query.parent_field.relation().is_many_to_many() { - true => m2m_join_alias_name(&nested_query.parent_field), - false => join_alias_name(&nested_query.parent_field), - }; + fn with_related_queries<'a, 'b>( + &mut self, + input: Select<'a>, + relation_selections: impl Iterator, + parent_alias: Alias, + ctx: &Context<'_>, + ) -> Select<'a> { + relation_selections.fold(input, |acc, rs| self.with_related_query(acc, rs, parent_alias, ctx)) + } - build_obj_params.push(( - Cow::from(nested_query.name.to_owned()), - Expression::from(Column::from((table_name, JSON_AGG_IDENT))), - )); + fn with_related_query<'a>( + &mut self, + select: Select<'a>, + rs: &RelationSelection, + parent_alias: Alias, + ctx: &Context<'_>, + ) -> Select<'a> { + if rs.field.relation().is_many_to_many() { + // m2m relations need to left join on the relation table first + let m2m_join = self.build_m2m_join(rs, parent_alias, ctx); + + select.left_join(m2m_join) + } else { + // LEFT JOIN LATERAL () AS ON TRUE + let join_select = Table::from(self.build_related_query_select(rs, parent_alias, ctx)) + .alias(join_alias_name(&rs.field)) + .on(ConditionTree::single(true.raw())) + .lateral(); + + select.left_join(join_select) } } - let inner_alias = join_alias_name(&rq.parent_field.related_field()); - - // SELECT JSON_BUILD_OBJECT() - let inner = Select::from_table(rq.parent_field.related_model().as_table(ctx)) - .value(json_build_object(build_obj_params).alias(JSON_AGG_IDENT)); - - // SELECT - let inner = ModelProjection::from(rq.parent_field.related_field().linking_fields()) - .as_columns(ctx) - .fold(inner, |acc, c| acc.column(c)); + fn build_related_query_select( + &mut self, + rs: &RelationSelection, + parent_alias: Alias, + ctx: &Context<'_>, + ) -> Select<'static> { + let table_alias = self.next_alias(); - let inner = with_join_conditions(inner, &rq.parent_field, ctx); + dbg!(&rs); - let inner = if let Some(nested) = rq.nested { - with_related_queries(inner, nested, ctx) - } else { - inner - }; - - if rq.parent_field.relation().is_many_to_many() { - // SELECT ONLY if it's a m2m table as we need to order by outside of the inner select - let inner = rq - .args - .order_by + let build_obj_params = rs + .selections .iter() - .flat_map(|order_by| match order_by { - prisma_models::OrderBy::Scalar(x) if x.path.is_empty() => vec![x.field.clone()], - prisma_models::OrderBy::Relevance(x) => x.fields.clone(), - _ => Vec::new(), + .filter_map(|f| match f { + SelectedField::Scalar(sf) => Some(( + Cow::from(sf.db_name().to_owned()), + Expression::from(sf.as_column(ctx).table(table_alias.to_table_string())), + )), + SelectedField::Relation(rs) => { + let table_name = match rs.field.relation().is_many_to_many() { + true => m2m_join_alias_name(&rs.field), + false => join_alias_name(&rs.field), + }; + + Some(( + Cow::from(rs.field.name().to_owned()), + Expression::from(Column::from((table_name, JSON_AGG_IDENT))), + )) + } + _ => None, }) - .fold(inner, |acc, sf| acc.column(sf.as_column(ctx))); - - inner - } else { - let inner = with_ordering(inner, &rq.args, None, ctx); - let inner = with_pagination(inner, rq.args.take, rq.args.skip); - let inner = with_filters(inner, rq.args.filter, ctx); - - let inner = Table::from(inner).alias(inner_alias.clone()); - let middle = Select::from_table(inner).column(Column::from((inner_alias.clone(), JSON_AGG_IDENT))); - let outer = Select::from_table(Table::from(middle).alias(format!("{}_1", inner_alias))).value(json_agg()); - - outer + .collect_vec(); + + let inner_alias = join_alias_name(&rs.field.related_field()); + + let related_table = rs + .field + .related_model() + .as_table(ctx) + .alias(table_alias.to_table_string()); + + // SELECT JSON_BUILD_OBJECT() + let inner = Select::from_table(related_table).value(json_build_object(build_obj_params).alias(JSON_AGG_IDENT)); + + // WHERE parent.id = child.parent_id + let inner = self.with_join_conditions(inner, &rs.field, parent_alias, table_alias, ctx); + // LEFT JOIN LATERAL () AS ON TRUE + let inner = self.with_related_queries(inner, rs.relations(), table_alias, ctx); + + let linking_fields = rs.field.related_field().linking_fields(); + + if rs.field.relation().is_many_to_many() { + let order_by_selection = rs + .args + .order_by + .iter() + .flat_map(|order_by| match order_by { + OrderBy::Scalar(x) if x.path.is_empty() => vec![x.field.clone()], + OrderBy::Relevance(x) => x.fields.clone(), + _ => Vec::new(), + }) + .collect_vec(); + let selection = FieldSelection::union(vec![FieldSelection::from(order_by_selection), linking_fields]); + + // SELECT + // SELECT ONLY if it's a m2m table as we need to order by outside of the inner select + let inner = ModelProjection::from(selection) + .as_columns(ctx) + .fold(inner, |acc, c| acc.column(c.table(table_alias.to_table_string()))); + + inner + } else { + // SELECT + let inner = ModelProjection::from(linking_fields) + .as_columns(ctx) + .fold(inner, |acc, c| acc.column(c.table(table_alias.to_table_string()))); + + let inner = self.with_ordering(inner, &rs.args, Some(table_alias.to_table_string()), ctx); + let inner = self.with_pagination(inner, rs.args.take_abs(), rs.args.skip); + let inner = self.with_filters(inner, rs.args.filter.clone(), Some(table_alias), ctx); + + let inner = Table::from(inner).alias(inner_alias.clone()); + let middle = Select::from_table(inner).column(Column::from((inner_alias.clone(), JSON_AGG_IDENT))); + let outer = Select::from_table(Table::from(middle).alias(format!("{}_1", inner_alias))).value(json_agg()); + + outer + } } -} - -fn build_m2m_join<'a>(rq: RelatedQuery, ctx: &Context<'_>) -> JoinData<'a> { - let rf = rq.parent_field.clone(); - let m2m_alias = m2m_join_alias_name(&rf); - - let left_columns = rf.related_field().m2m_columns(ctx); - let right_columns = ModelProjection::from(rf.model().primary_identifier()).as_columns(ctx); - let conditions = left_columns - .into_iter() - .zip(right_columns) - .fold(None::, |acc, (a, b)| match acc { - Some(acc) => Some(acc.and(a.equals(b))), - None => Some(a.equals(b).into()), - }) - .unwrap(); - - let inner = Select::from_table(rf.as_table(ctx)) - .value(Column::from((join_alias_name(&rf), JSON_AGG_IDENT))) - .and_where(conditions); + fn build_m2m_join<'a>(&mut self, rs: &RelationSelection, parent_alias: Alias, ctx: &Context<'_>) -> JoinData<'a> { + let rf = rs.field.clone(); + let m2m_alias = m2m_join_alias_name(&rf); + let m2m_table_alias = self.next_alias(); + + let left_columns = rf.related_field().m2m_columns(ctx); + let right_columns = ModelProjection::from(rf.model().primary_identifier()).as_columns(ctx); + + let conditions = left_columns + .into_iter() + .zip(right_columns) + .fold(None::, |acc, (a, b)| { + let a = a.table(m2m_table_alias.to_table_string()); + let b = b.table(parent_alias.to_table_string()); + let condition = a.equals(b); + + match acc { + Some(acc) => Some(acc.and(condition)), + None => Some(condition.into()), + } + }) + .unwrap(); - let inner = with_ordering(inner, &rq.args, Some(join_alias_name(&rq.parent_field)), ctx); - let inner = with_pagination(inner, rq.args.take, rq.args.skip); - // TODO: avoid clone? - let inner = with_filters(inner, rq.args.filter.clone(), ctx); + let inner = Select::from_table(rf.as_table(ctx).alias(m2m_table_alias.to_table_string())) + .value(Column::from((join_alias_name(&rf), JSON_AGG_IDENT))) + .and_where(conditions); - let join_select = Table::from(build_related_query_select(rq, ctx)) - .alias(join_alias_name(&rf)) - .on(ConditionTree::single(true.raw())) - .lateral(); + let inner = self.with_ordering(inner, &rs.args, Some(join_alias_name(&rs.field)), ctx); + let inner = self.with_pagination(inner, rs.args.take_abs(), rs.args.skip); + // TODO: avoid clone? + let inner = self.with_filters(inner, rs.args.filter.clone(), None, ctx); - let inner = inner.left_join(join_select); + // TODO: parent_alias is likely wrong here + let join_select = Table::from(self.build_related_query_select(rs, m2m_table_alias, ctx)) + .alias(join_alias_name(&rf)) + .on(ConditionTree::single(true.raw())) + .lateral(); - let outer = Select::from_table(Table::from(inner).alias(format!("{}_1", m2m_alias))).value(json_agg()); + let inner = inner.left_join(join_select); - Table::from(outer) - .alias(m2m_alias) - .on(ConditionTree::single(true.raw())) - .lateral() -} + let outer = Select::from_table(Table::from(inner).alias(format!("{}_1", m2m_alias))).value(json_agg()); -fn json_agg() -> Function<'static> { - coalesce(vec![ - json_array_agg(Column::from(JSON_AGG_IDENT)).into(), - Expression::from("[]".raw()), - ]) - .alias(JSON_AGG_IDENT) -} - -/// Builds the lateral join conditions -fn with_join_conditions<'a>(select: Select<'a>, rf: &RelationField, ctx: &Context<'_>) -> Select<'a> { - let join_columns = rf.join_columns(ctx); - // .map(|c| c.opt_table(is_m2m.then(|| m2m_join_alias_name(rf)))); - let related_join_columns = ModelProjection::from(rf.related_field().linking_fields()).as_columns(ctx); - - // WHERE Parent.id = Child.id - let conditions = join_columns - .zip(related_join_columns) - .fold(None::, |acc, (a, b)| match acc { - Some(acc) => Some(acc.and(a.equals(b))), - None => Some(a.equals(b).into()), - }) - .unwrap(); - - select.and_where(conditions) -} + Table::from(outer) + .alias(m2m_alias) + .on(ConditionTree::single(true.raw())) + .lateral() + } -fn with_ordering<'a>( - select: Select<'a>, - args: &QueryArguments, - parent_alias: Option, - ctx: &Context<'_>, -) -> Select<'a> { - let order_by_definitions = OrderByBuilder::default() - .with_parent_alias(parent_alias) - .build(args, ctx); - - let select = order_by_definitions - .iter() - .flat_map(|j| &j.joins) - .fold(select, |acc, join| acc.join(join.clone().data)); - - order_by_definitions - .iter() - .fold(select, |acc, o| acc.order_by(o.order_definition.clone())) -} + /// Builds the lateral join conditions + fn with_join_conditions<'a>( + &mut self, + select: Select<'a>, + rf: &RelationField, + parent_alias: Alias, + child_alias: Alias, + ctx: &Context<'_>, + ) -> Select<'a> { + let join_columns = rf.join_columns(ctx); + let related_join_columns = ModelProjection::from(rf.related_field().linking_fields()).as_columns(ctx); + + // WHERE Parent.id = Child.id + let conditions = join_columns + .zip(related_join_columns) + .fold(None::, |acc, (a, b)| { + let a = a.table(parent_alias.to_table_string()); + let b = b.table(child_alias.to_table_string()); + let condition = a.equals(b); + + match acc { + Some(acc) => Some(acc.and(condition)), + None => Some(condition.into()), + } + }) + .unwrap(); -fn with_pagination<'a>(select: Select<'a>, take: Option, skip: Option) -> Select<'a> { - let select = match take { - Some(take) => select.limit(take as usize), - None => select, - }; + select.and_where(conditions) + } - let select = match skip { - Some(skip) => select.offset(skip as usize), - None => select, - }; + fn with_ordering<'a>( + &mut self, + select: Select<'a>, + args: &QueryArguments, + parent_alias: Option, + ctx: &Context<'_>, + ) -> Select<'a> { + let order_by_definitions = OrderByBuilder::default() + .with_parent_alias(parent_alias) + .build(args, ctx); + + let select = order_by_definitions + .iter() + .flat_map(|j| &j.joins) + .fold(select, |acc, join| acc.join(join.clone().data)); - select -} + order_by_definitions + .iter() + .fold(select, |acc, o| acc.order_by(o.order_definition.clone())) + } -fn with_filters<'a>(select: Select<'a>, filter: Option, ctx: &Context<'_>) -> Select<'a> { - if let Some(filter) = filter { - let (filter, joins) = FilterBuilder::with_top_level_joins().visit_filter(filter, ctx); - let select = select.and_where(filter); + fn with_pagination<'a>(&mut self, select: Select<'a>, take: Option, skip: Option) -> Select<'a> { + let select = match take { + Some(take) => select.limit(take as usize), + None => select, + }; - let select = match joins { - Some(joins) => joins.into_iter().fold(select, |acc, join| acc.join(join.data)), + let select = match skip { + Some(skip) => select.offset(skip as usize), None => select, }; select - } else { - select + } + + fn with_filters<'a>( + &mut self, + select: Select<'a>, + filter: Option, + alias: Option, + ctx: &Context<'_>, + ) -> Select<'a> { + use crate::filter::*; + + if let Some(filter) = filter { + let mut visitor = crate::filter::FilterVisitor::with_top_level_joins().set_parent_alias_opt(alias); + let (filter, joins) = visitor.visit_filter(filter, ctx); + let select = select.and_where(filter); + + let select = match joins { + Some(joins) => joins.into_iter().fold(select, |acc, join| acc.join(join.data)), + None => select, + }; + + select + } else { + select + } } } @@ -270,3 +328,11 @@ fn join_alias_name(rf: &RelationField) -> String { fn m2m_join_alias_name(rf: &RelationField) -> String { format!("{}_{}_m2m", rf.model().name(), rf.name()) } + +fn json_agg() -> Function<'static> { + coalesce(vec![ + json_array_agg(Column::from(JSON_AGG_IDENT)).into(), + Expression::from("[]".raw()), + ]) + .alias(JSON_AGG_IDENT) +} diff --git a/query-engine/core/src/response_ir/internal.rs b/query-engine/core/src/response_ir/internal.rs index fd77628e353..f5e8417aac4 100644 --- a/query-engine/core/src/response_ir/internal.rs +++ b/query-engine/core/src/response_ir/internal.rs @@ -394,7 +394,7 @@ fn serialize_objects_with_relation( serialize_relation_selection(rrs, val, inner_typ, query_schema)?, ); } - _ => (), + _ => panic!("unexpected field"), } } @@ -413,6 +413,10 @@ fn serialize_relation_selection( typ: &ObjectType<'_>, query_schema: &QuerySchema, ) -> crate::Result { + if value.is_null() { + return Ok(Item::Value(PrismaValue::Null)); + } + let mut map = Map::new(); // TODO: handle errors