Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support 1-m relevance ordering #4915

Merged
merged 2 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<()> {
Weakky marked this conversation as resolved.
Show resolved Hide resolved
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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relations weren't taken into account at all. We're now gathering the path (which is an array of relation fields) that leads to the fields we try to order by on. We compute the needed joins, and then we order by the last field, aliased by the last join alias. This is the same process as regular relational ordering on scalar field.


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
Loading