Skip to content

Commit

Permalink
fix: support 1-m relevance ordering (#4915)
Browse files Browse the repository at this point in the history
  • Loading branch information
Weakky authored Jun 24, 2024
1 parent d56fe2e commit 293bb8f
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,92 @@ async fn on_many_fields_with_aggr_and_pagination(runner: Runner) -> TestResult<(
Ok(())
}

async fn on_1m_relation_field(runner: Runner) -> TestResult<()> {
create_row(
&runner,
r#"{ id: 1, fieldA: "developer", fieldB: "developer developer developer", relations: { create: [{ id: 1 }] }}"#,
)
.await?;
create_row(
&runner,
r#"{ id: 2, fieldA: "developer developer", fieldB: "developer", relations: { create: [{ id: 2 }] }}"#,
)
.await?;
create_row(
&runner,
r#"{ id: 3, fieldA: "a developer", fieldB: "developer", fieldC: "developer", relations: { create: [{ id: 3 }] }}"#,
)
.await?;

// Single field required
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: fieldA, search: "developer", sort: desc } } }, { id: desc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":2},{"id":3},{"id":1}]}}"###
);
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: fieldA, search: "developer", sort: asc } } }, { id: asc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":1},{"id":3},{"id":2}]}}"###
);

// Single field optional
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: fieldC, search: "developer", sort: desc } } }, { id: desc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":3},{"id":2},{"id":1}]}}"###
);
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: fieldC, search: "developer", sort: asc } } }, { id: asc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":1},{"id":2},{"id":3}]}}"###
);

// Many fields required
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: [fieldA, fieldB], search: "developer", sort: desc } } }, { id: desc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":1},{"id":2},{"id":3}]}}"###
);
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: [fieldA, fieldB], search: "developer", sort: asc } } }, { id: asc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":3},{"id":2},{"id":1}]}}"###
);

// Many fields optional
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: [fieldB, fieldC], search: "developer", sort: desc } } }, { id: desc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":1},{"id":3},{"id":2}]}}"###
);
insta::assert_snapshot!(
run_query!(&runner, r#"{ findManyRelation(orderBy: [{ testModel: { _relevance: { fields: [fieldB, fieldC], search: "developer", sort: asc } } }, { id: asc }]) { id } }"#),
@r###"{"data":{"findManyRelation":[{"id":2},{"id":3},{"id":1}]}}"###
);

// Many fields optional with cursor
insta::assert_snapshot!(
run_query!(&runner, r#"{
findManyRelation(
orderBy: {
testModel: { _relevance: { fields: [fieldB, fieldC], search: "developer", sort: desc } }
}
cursor: { id: 3 },
skip: 1
) { id } }
"#),
@r###"{"data":{"findManyRelation":[{"id":2}]}}"###
);
insta::assert_snapshot!(
run_query!(&runner, r#"{
findManyRelation(
orderBy: {
testModel: { _relevance: { fields: [fieldB, fieldC], search: "developer", sort: asc } }
}
cursor: { id: 3 },
skip: 1
) { id } }
"#),
@r###"{"data":{"findManyRelation":[{"id":1}]}}"###
);

Ok(())
}

async fn create_test_data(runner: &Runner) -> TestResult<()> {
create_row(
runner,
Expand Down Expand Up @@ -479,6 +565,11 @@ mod order_by_relevance_without_index {
async fn on_many_fields_aggr_pagination(runner: Runner) -> TestResult<()> {
super::on_many_fields_with_aggr_and_pagination(runner).await
}

#[connector_test]
async fn on_1m_relation_field(runner: Runner) -> TestResult<()> {
super::on_1m_relation_field(runner).await
}
}

#[test_suite(schema(schema), capabilities(FullTextSearchWithIndex))]
Expand Down Expand Up @@ -561,4 +652,9 @@ mod order_by_relevance_with_index {
async fn on_many_fields_aggr_pagination(runner: Runner) -> TestResult<()> {
super::on_many_fields_with_aggr_and_pagination(runner).await
}

#[connector_test]
async fn on_1m_relation_field(runner: Runner) -> TestResult<()> {
super::on_1m_relation_field(runner).await
}
}
90 changes: 66 additions & 24 deletions query-engine/connectors/sql-query-connector/src/ordering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ pub(crate) struct OrderByDefinition {

#[derive(Debug, Default)]
pub(crate) struct OrderByBuilder {
/// Parent table alias, used mostly for relationLoadStrategy: join when performing nested ordering.
/// This parent alias enables us to prefix the ordered field with the correct parent join alias.
parent_alias: Option<String>,
// Used to generate unique join alias
/// Counter used to generate unique join alias
join_counter: usize,
}

Expand Down Expand Up @@ -81,19 +83,14 @@ impl OrderByBuilder {
needs_reversed_order: bool,
ctx: &Context<'_>,
) -> OrderByDefinition {
let columns: Vec<Expression> = order_by
.fields
.iter()
.map(|sf| sf.as_column(ctx).opt_table(self.parent_alias.clone()).into())
.collect();
let order_column: Expression = text_search_relevance(&columns, order_by.search.clone()).into();
let (joins, order_column) = self.compute_joins_relevance(order_by, ctx);
let order: Option<Order> = Some(into_order(&order_by.sort_order, None, needs_reversed_order));
let order_definition: OrderDefinition = (order_column.clone(), order);

OrderByDefinition {
order_column,
order_definition,
joins: vec![],
joins,
}
}

Expand Down Expand Up @@ -200,33 +197,78 @@ impl OrderByBuilder {
order_by: &OrderByScalar,
ctx: &Context<'_>,
) -> (Vec<AliasedJoin>, Column<'static>) {
let mut joins: Vec<AliasedJoin> = vec![];
let parent_alias = self.parent_alias.clone();
let joins: Vec<AliasedJoin> = self.compute_one2m_join(&order_by.path, parent_alias.as_ref(), ctx);

// This is the final column identifier to be used for the scalar field to order by.
// - If we order by a scalar field on the base model, we simply use the model's scalar field. eg:
// `{modelTable}.{field}`
// - If there's a parent_alias, we use it to prefix the field, e.g. `{parent_alias}.{field}`
// - If we order by some relations, we use the alias used for the last join, e.g.
// `{join_alias}.{field}`
let parent_table = joins
.last()
.map(|j| j.alias.to_owned())
.or_else(|| self.parent_alias.clone());
let order_by_column = order_by.field.as_column(ctx).opt_table(parent_table);

(joins, order_by_column)
}

pub(crate) fn compute_joins_relevance(
&mut self,
order_by: &OrderByRelevance,
ctx: &Context<'_>,
) -> (Vec<AliasedJoin>, Expression<'static>) {
let parent_alias = self.parent_alias.clone();
let joins: Vec<AliasedJoin> = self.compute_one2m_join(&order_by.path, parent_alias.as_ref(), ctx);

for (i, hop) in order_by.path.iter().enumerate() {
// This is the final column identifier to be used for the scalar field to order by.
// - If we order by a scalar field on the base model, we simply use the model's scalar field. eg:
// `{modelTable}.{field}`
// - If there's a parent_alias, we use it to prefix the field, e.g. `{parent_alias}.{field}`
// - If we order by some relations, we use the alias used for the last join, e.g.
// `{join_alias}.{field}`
let parent_table = joins
.last()
.map(|j| j.alias.to_owned())
.or_else(|| self.parent_alias.clone());
let order_by_columns: Vec<_> = order_by
.fields
.iter()
.map(|sf| sf.as_column(ctx).opt_table(parent_table.clone()))
.map(Expression::from)
.collect();
let text_search_expr = text_search_relevance(&order_by_columns, order_by.search.clone());

(joins, text_search_expr.into())
}

fn compute_one2m_join(
&mut self,
path: &[OrderByHop],
parent_alias: Option<&String>,
ctx: &Context<'_>,
) -> Vec<AliasedJoin> {
let mut joins: Vec<AliasedJoin> = vec![];

for (i, hop) in path.iter().enumerate() {
let previous_join = if i > 0 { joins.get(i - 1) } else { None };
let previous_alias = previous_join
.map(|j| &j.alias)
.or(parent_alias.as_ref())
.or(parent_alias)
.map(|alias| alias.as_str());
let join = compute_one2m_join(hop.as_relation_hop().unwrap(), &self.join_prefix(), previous_alias, ctx);
let join = crate::join_utils::compute_one2m_join(
hop.as_relation_hop().unwrap(),
&self.join_prefix(),
previous_alias,
ctx,
);

joins.push(join);
}

// This is the final column identifier to be used for the scalar field to order by.
// - If we order by a scalar field on the base model, we simply use the model's scalar field. eg:
// `{modelTable}.{field}`
// - If we order by some relations, we use the alias used for the last join, e.g.
// `{join_alias}.{field}`
let order_by_column = if let Some(last_join) = joins.last() {
Column::from((last_join.alias.to_owned(), order_by.field.db_name().to_owned()))
} else {
order_by.field.as_column(ctx).opt_table(self.parent_alias.clone())
};

(joins, order_by_column)
joins
}

fn join_prefix(&mut self) -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ fn process_order_object(
if field_name.as_ref() == ordering::UNDERSCORE_RELEVANCE {
let object: ParsedInputMap<'_> = field_value.try_into()?;

return extract_order_by_relevance(container, object);
return extract_order_by_relevance(container, object, path);
}

if let Some(sort_aggr) = extract_sort_aggregation(field_name.as_ref()) {
Expand Down Expand Up @@ -172,6 +172,7 @@ fn process_order_object(
fn extract_order_by_relevance(
container: &ParentContainer,
object: ParsedInputMap<'_>,
path: Vec<OrderByHop>,
) -> QueryGraphBuilderResult<Option<OrderBy>> {
let (sort_order, _) = extract_order_by_args(object.get(ordering::SORT).unwrap().clone())?;
let search: PrismaValue = object.get(ordering::SEARCH).unwrap().clone().try_into()?;
Expand All @@ -198,7 +199,7 @@ fn extract_order_by_relevance(
})
.collect::<Result<Vec<ScalarFieldRef>, _>>()?;

Ok(Some(OrderBy::relevance(fields, search, sort_order)))
Ok(Some(OrderBy::relevance(fields, search, sort_order, path)))
}

fn extract_sort_aggregation(field_name: &str) -> Option<SortAggregation> {
Expand Down
9 changes: 8 additions & 1 deletion query-engine/query-structure/src/order_by.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,17 @@ impl OrderBy {
})
}

pub fn relevance(fields: Vec<ScalarFieldRef>, search: String, sort_order: SortOrder) -> Self {
pub fn relevance(
fields: Vec<ScalarFieldRef>,
search: String,
sort_order: SortOrder,
path: Vec<OrderByHop>,
) -> Self {
Self::Relevance(OrderByRelevance {
fields,
sort_order,
search,
path,
})
}
}
Expand Down Expand Up @@ -201,6 +207,7 @@ pub struct OrderByRelevance {
pub fields: Vec<ScalarFieldRef>,
pub sort_order: SortOrder,
pub search: String,
pub path: Vec<OrderByHop>,
}

impl Display for SortOrder {
Expand Down

0 comments on commit 293bb8f

Please sign in to comment.