#![allow(clippy::unwrap_used)]
use super::*;
use crate::types::sql_hints::OrderDirection;
#[test]
fn test_append_order_by_none() {
let mut sql = "SELECT data FROM v_user".to_string();
let bound =
append_order_by(&mut sql, None, DatabaseType::PostgreSQL, 1, Tiebreak::Identity).unwrap();
assert!(bound.is_empty());
assert!(!sql.contains("ORDER BY"));
}
#[test]
fn test_append_order_by_empty() {
let mut sql = "SELECT data FROM v_user".to_string();
let bound =
append_order_by(&mut sql, Some(&[]), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty());
assert!(!sql.contains("ORDER BY"));
}
#[test]
fn test_append_order_by_single_clause_postgres() {
let mut sql = "SELECT data FROM v_user".to_string();
let clauses = [OrderByClause::new(
"createdAt".to_string(),
OrderDirection::Desc,
)];
let bound =
append_order_by(&mut sql, Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(
sql,
"SELECT data FROM v_user ORDER BY data->>'created_at' DESC, data->>'id' ASC"
);
}
#[test]
fn test_append_order_by_multiple_clauses_postgres() {
let mut sql = "SELECT data FROM v_user".to_string();
let clauses = [
OrderByClause::new("lastName".to_string(), OrderDirection::Asc),
OrderByClause::new("createdAt".to_string(), OrderDirection::Desc),
];
let bound =
append_order_by(&mut sql, Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(
sql,
"SELECT data FROM v_user ORDER BY data->>'last_name' ASC, data->>'created_at' DESC, \
data->>'id' ASC"
);
}
#[test]
fn test_append_order_by_invalid_field_name() {
let mut sql = "SELECT data FROM v_user".to_string();
let clauses = [OrderByClause::new(
"field'; DROP TABLE users; --".to_string(),
OrderDirection::Asc,
)];
let result =
append_order_by(&mut sql, Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::Identity);
assert!(result.is_err());
}
#[test]
fn test_append_order_by_snake_case_passthrough() {
let mut sql = "SELECT data FROM v_user".to_string();
let clauses = [OrderByClause::new("id".to_string(), OrderDirection::Asc)];
let bound =
append_order_by(&mut sql, Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(sql, "SELECT data FROM v_user ORDER BY data->>'id' ASC");
}
#[test]
fn test_append_order_by_numeric_cast_postgres() {
use crate::types::sql_hints::ScalarFieldType;
let mut sql = "SELECT data FROM v_order".to_string();
let mut clause = OrderByClause::new("totalAmount".to_string(), OrderDirection::Desc);
clause.field_type = ScalarFieldType::Numeric;
let bound =
append_order_by(&mut sql, Some(&[clause]), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(
sql,
"SELECT data FROM v_order ORDER BY (data->>'total_amount')::numeric DESC, data->>'id' ASC"
);
}
#[test]
fn test_append_order_by_datetime_cast_postgres() {
use crate::types::sql_hints::ScalarFieldType;
let mut sql = "SELECT data FROM v_event".to_string();
let mut clause = OrderByClause::new("createdAt".to_string(), OrderDirection::Desc);
clause.field_type = ScalarFieldType::DateTime;
let bound =
append_order_by(&mut sql, Some(&[clause]), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(
sql,
"SELECT data FROM v_event ORDER BY (data->>'created_at')::timestamptz DESC, data->>'id' ASC"
);
}
#[test]
fn test_append_order_by_native_column() {
let mut sql = "SELECT data FROM tv_user".to_string();
let clause = OrderByClause {
field: "createdAt".to_string(),
direction: OrderDirection::Desc,
field_type: crate::types::sql_hints::ScalarFieldType::DateTime,
native_column: Some("created_at".to_string()),
vector: None,
relevance: None,
identity: false,
};
let bound =
append_order_by(&mut sql, Some(&[clause]), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(sql, "SELECT data FROM tv_user ORDER BY created_at DESC, data->>'id' ASC");
}
#[test]
fn test_append_order_by_mixed_native_and_jsonb() {
use crate::types::sql_hints::ScalarFieldType;
let mut sql = "SELECT data FROM tv_user".to_string();
let clauses = [
OrderByClause {
field: "createdAt".to_string(),
direction: OrderDirection::Desc,
field_type: ScalarFieldType::DateTime,
native_column: Some("created_at".to_string()),
vector: None,
relevance: None,
identity: false,
},
{
let mut c = OrderByClause::new("name".to_string(), OrderDirection::Asc);
c.field_type = ScalarFieldType::Text;
c
},
];
let bound =
append_order_by(&mut sql, Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap();
assert!(bound.is_empty(), "a field ordering binds nothing");
assert_eq!(
sql,
"SELECT data FROM tv_user ORDER BY created_at DESC, data->>'name' ASC, data->>'id' ASC"
);
}
#[test]
fn test_render_order_by_columns_none() {
assert!(
render_order_by_columns(None, DatabaseType::PostgreSQL, 1, Tiebreak::None)
.unwrap()
.is_none()
);
}
#[test]
fn test_render_order_by_columns_empty() {
assert!(
render_order_by_columns(Some(&[]), DatabaseType::PostgreSQL, 1, Tiebreak::None)
.unwrap()
.is_none()
);
}
#[test]
fn test_render_order_by_columns_no_keyword_prefix() {
let clauses = [
OrderByClause::new("lastName".to_string(), OrderDirection::Asc),
OrderByClause::new("createdAt".to_string(), OrderDirection::Desc),
];
let cols = render_order_by_columns(Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::None)
.unwrap()
.unwrap()
.columns;
assert_eq!(cols, "data->>'last_name' ASC, data->>'created_at' DESC");
assert!(!cols.contains("ORDER BY"));
}
#[test]
fn test_render_order_by_columns_matches_append_body() {
let clauses = [OrderByClause::new(
"createdAt".to_string(),
OrderDirection::Desc,
)];
let cols = render_order_by_columns(Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::None)
.unwrap()
.unwrap()
.columns;
let mut sql = String::new();
append_order_by(&mut sql, Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::None).unwrap();
assert_eq!(sql, format!(" ORDER BY {cols}"));
}
#[test]
fn test_render_order_by_columns_invalid_field_name() {
let clauses = [OrderByClause::new(
"field'; DROP TABLE users; --".to_string(),
OrderDirection::Asc,
)];
assert!(
render_order_by_columns(Some(&clauses), DatabaseType::PostgreSQL, 1, Tiebreak::None)
.is_err()
);
}
fn vector_order(operator: &str, literal: &str, kind: VectorOperandKind) -> OrderByClause {
let mut clause = OrderByClause::new("embedding".to_string(), OrderDirection::Asc);
clause.native_column = Some("\"embedding\"".to_string());
clause.vector = Some(crate::types::sql_hints::VectorDistanceOrder {
operator: operator.to_string(),
query_vector: literal.to_string(),
kind,
});
clause
}
#[test]
fn float_vector_order_casts_to_vector() {
let cols = render_order_by_columns(
Some(&[vector_order("<=>", "[1,0,0.5]", VectorOperandKind::Float)]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap()
.unwrap()
.columns;
assert_eq!(cols, "\"embedding\" <=> '[1,0,0.5]'::vector ASC");
}
#[test]
fn bit_vector_order_casts_to_varbit() {
for (op, expected) in [("<~>", "<~>"), ("<%>", "<%>")] {
let cols = render_order_by_columns(
Some(&[vector_order(op, "1011", VectorOperandKind::Bit)]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap()
.unwrap()
.columns;
assert_eq!(cols, format!("\"embedding\" {expected} '1011'::varbit ASC"));
}
}
#[test]
fn a_vector_operator_of_the_other_kind_is_refused() {
for (op, literal, kind) in [
("<~>", "[1,0]", VectorOperandKind::Float),
("<=>", "1011", VectorOperandKind::Bit),
] {
let err = render_order_by_columns(
Some(&[vector_order(op, literal, kind)]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap_err()
.to_string();
assert!(err.contains("unsupported vector distance operator"), "got: {err}");
}
}
#[test]
fn a_malformed_bit_literal_is_refused() {
for literal in ["10x1", "", "[1,0]"] {
let err = render_order_by_columns(
Some(&[vector_order("<~>", literal, VectorOperandKind::Bit)]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap_err()
.to_string();
assert!(err.contains("malformed vector literal"), "got for {literal:?}: {err}");
}
}
use crate::types::sql_hints::RelevanceOrder;
fn relevance_order(fields: &[&str], query: &str) -> OrderByClause {
OrderByClause::by_relevance(RelevanceOrder {
fields: fields.iter().map(|f| (*f).to_string()).collect(),
query: query.to_string(),
})
}
#[test]
fn a_relevance_clause_ranks_by_ts_rank_and_binds_its_query() {
let rendered = render_order_by_columns(
Some(&[relevance_order(&["label"], "row-42")]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap()
.unwrap();
assert_eq!(
rendered.columns,
"ts_rank(to_tsvector(coalesce(data->>'label', '')), websearch_to_tsquery($1)) DESC"
);
assert_eq!(rendered.params, vec!["row-42".to_string()]);
assert!(
!rendered.columns.contains("row-42"),
"the search text must be bound, not interpolated: {}",
rendered.columns
);
}
#[test]
fn a_relevance_clause_binds_at_the_index_the_caller_offers() {
let mut sql = "SELECT data FROM v_doc WHERE to_tsvector(data->>'label') @@ \
websearch_to_tsquery($1)"
.to_string();
let bound = append_order_by(
&mut sql,
Some(&[relevance_order(&["label"], "x")]),
DatabaseType::PostgreSQL,
2,
Tiebreak::Identity,
)
.unwrap();
assert!(sql.contains("websearch_to_tsquery($2)) DESC"), "{sql}");
assert!(
sql.ends_with("DESC, data->>'id' ASC"),
"a ts_rank ordering is tie-broken too: {sql}"
);
assert_eq!(bound, vec!["x".to_string()]);
}
#[test]
fn a_multi_field_relevance_clause_coalesces_every_operand() {
let rendered = render_order_by_columns(
Some(&[relevance_order(&["title", "body"], "q")]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap()
.unwrap();
assert_eq!(
rendered.columns,
"ts_rank(to_tsvector(coalesce(data->>'title', '') || ' ' || coalesce(data->>'body', '')), \
websearch_to_tsquery($1)) DESC"
);
}
#[test]
fn a_relevance_field_name_passes_the_identifier_boundary() {
let err = render_order_by_columns(
Some(&[relevance_order(&["label'; DROP TABLE users; --"], "q")]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap_err()
.to_string();
assert!(err.contains("orderBy"), "got: {err}");
}
#[test]
fn a_relevance_clause_with_no_fields_is_refused() {
let err = render_order_by_columns(
Some(&[relevance_order(&[], "q")]),
DatabaseType::PostgreSQL,
1,
Tiebreak::None,
)
.unwrap_err()
.to_string();
assert!(err.contains("at least one searchable field"), "got: {err}");
}
#[test]
fn only_a_relevance_clause_reports_that_it_binds_a_parameter() {
assert!(relevance_order(&["label"], "q").binds_parameter());
assert!(!OrderByClause::new("id".to_string(), OrderDirection::Asc).binds_parameter());
assert!(!vector_order("<=>", "[1,0]", VectorOperandKind::Float).binds_parameter());
}
#[test]
fn cursor_pagination_refuses_a_relevance_ordering() {
let err = refuse_relevance_under_cursor_pagination(Some(&[relevance_order(&["label"], "q")]))
.unwrap_err()
.to_string();
assert!(err.contains("not a resumable sort key"), "got: {err}");
assert!(
refuse_relevance_under_cursor_pagination(Some(&[OrderByClause::new(
"id".to_string(),
OrderDirection::Asc
)]))
.is_ok()
);
assert!(refuse_relevance_under_cursor_pagination(None).is_ok());
}
mod pagination_tiebreak {
use super::*;
fn cols(clauses: &[OrderByClause], tiebreak: Tiebreak) -> String {
render_order_by_columns(Some(clauses), DatabaseType::PostgreSQL, 1, tiebreak)
.unwrap()
.unwrap()
.columns
}
#[test]
fn a_non_unique_ordering_gains_the_identity() {
let clauses = [OrderByClause::new(
"status".to_string(),
OrderDirection::Asc,
)];
assert_eq!(cols(&clauses, Tiebreak::Identity), "data->>'status' ASC, data->>'id' ASC");
}
#[test]
fn tiebreak_none_renders_exactly_the_clauses_given() {
let clauses = [OrderByClause::new(
"status".to_string(),
OrderDirection::Asc,
)];
assert_eq!(cols(&clauses, Tiebreak::None), "data->>'status' ASC");
}
#[test]
fn an_ordering_that_already_names_the_identity_is_left_alone() {
let clauses = [OrderByClause::new("id".to_string(), OrderDirection::Asc)];
assert_eq!(cols(&clauses, Tiebreak::Identity), "data->>'id' ASC");
}
#[test]
fn the_identity_counts_wherever_it_sits_in_the_ordering() {
let clauses = [
OrderByClause::new("status".to_string(), OrderDirection::Asc),
OrderByClause::new("id".to_string(), OrderDirection::Desc),
];
assert_eq!(cols(&clauses, Tiebreak::Identity), "data->>'status' ASC, data->>'id' DESC");
}
fn native(field: &str, column: &str) -> OrderByClause {
OrderByClause {
field: field.to_string(),
direction: OrderDirection::Asc,
field_type: crate::types::sql_hints::ScalarFieldType::Text,
native_column: Some(column.to_string()),
vector: None,
relevance: None,
identity: false,
}
}
#[test]
fn a_native_id_column_counts_as_the_identity() {
assert_eq!(cols(&[native("id", "id")], Tiebreak::Identity), "id ASC");
}
#[test]
fn a_field_named_id_reading_another_native_column_does_not_count() {
assert_eq!(
cols(&[native("id", "tenant_id")], Tiebreak::Identity),
"tenant_id ASC, data->>'id' ASC"
);
}
#[test]
fn a_field_whose_name_merely_ends_in_id_is_not_the_identity() {
let clauses = [OrderByClause::new(
"userId".to_string(),
OrderDirection::Asc,
)];
assert_eq!(cols(&clauses, Tiebreak::Identity), "data->>'user_id' ASC, data->>'id' ASC");
}
#[test]
fn a_relevance_ordering_is_tie_broken() {
let rendered = render_order_by_columns(
Some(&[relevance_order(&["label"], "q")]),
DatabaseType::PostgreSQL,
1,
Tiebreak::Identity,
)
.unwrap()
.unwrap();
assert!(rendered.columns.ends_with("DESC, data->>'id' ASC"), "{}", rendered.columns);
assert_eq!(rendered.params, vec!["q".to_string()]);
}
#[test]
fn the_tiebreaker_binds_no_parameter() {
let clauses = [OrderByClause::new(
"status".to_string(),
OrderDirection::Asc,
)];
let rendered = render_order_by_columns(
Some(&clauses),
DatabaseType::PostgreSQL,
7,
Tiebreak::Identity,
)
.unwrap()
.unwrap();
assert!(rendered.params.is_empty());
}
#[test]
fn a_declared_identity_is_rendered_and_not_tie_broken_again() {
let clauses = [
OrderByClause::new("status".to_string(), OrderDirection::Asc),
OrderByClause::identity("pk_user".to_string(), Some("pk_user".to_string())),
];
assert_eq!(cols(&clauses, Tiebreak::Identity), "data->>'status' ASC, pk_user ASC");
}
#[test]
fn a_declared_json_identity_renders_the_same_term_the_tiebreaker_would() {
let declared = cols(&[OrderByClause::identity("id".to_string(), None)], Tiebreak::Identity);
let inferred = cols(
&[OrderByClause::new(
"status".to_string(),
OrderDirection::Asc,
)],
Tiebreak::Identity,
);
assert_eq!(declared, "data->>'id' ASC");
assert!(inferred.ends_with("data->>'id' ASC"), "{inferred}");
}
#[test]
fn an_undeclared_ordering_still_falls_back_to_the_json_identity() {
let clauses = [OrderByClause::new(
"status".to_string(),
OrderDirection::Asc,
)];
assert_eq!(cols(&clauses, Tiebreak::Identity), "data->>'status' ASC, data->>'id' ASC");
}
#[test]
fn the_declared_identity_is_ignored_under_tiebreak_none() {
let clauses = [
OrderByClause::new("status".to_string(), OrderDirection::Asc),
OrderByClause::identity("pk_user".to_string(), Some("pk_user".to_string())),
];
assert_eq!(cols(&clauses, Tiebreak::None), "data->>'status' ASC, pk_user ASC");
}
#[test]
fn orders_by_identity_reads_the_marker_before_the_name() {
assert!(orders_by_identity(&[OrderByClause::identity(
"pk_user".to_string(),
Some("pk_user".to_string())
)]));
assert!(orders_by_identity(&[OrderByClause::new("id".to_string(), OrderDirection::Asc)]));
assert!(!orders_by_identity(&[OrderByClause::new(
"status".to_string(),
OrderDirection::Asc
)]));
}
#[test]
fn no_requested_ordering_still_renders_nothing() {
assert!(
render_order_by_columns(None, DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap()
.is_none()
);
assert!(
render_order_by_columns(Some(&[]), DatabaseType::PostgreSQL, 1, Tiebreak::Identity)
.unwrap()
.is_none()
);
}
}