#![allow(clippy::panic)]
use fraiseql_core::schema::{AutoParams, PaginationOrder, QueryDefinition};
use fraiseql_db::postgres::IndexInfo;
use super::{Advice, advise, tie_break_key};
fn index(name: &str, keys: &[&str]) -> IndexInfo {
IndexInfo {
name: name.to_string(),
unique: false,
keys: keys.iter().map(|k| (*k).to_string()).collect(),
}
}
fn invoices() -> QueryDefinition {
let mut q = QueryDefinition::new("invoices", "Invoice");
q.sql_source = Some("v_invoice".to_string());
q.pagination_order = Some(PaginationOrder::Column("pk_invoice".to_string()));
q.auto_params = AutoParams {
has_order_by: true,
has_limit: true,
..AutoParams::default()
};
q.native_columns
.insert("status".to_string(), fraiseql_core::schema::NativeColumn::nullable("text"));
q
}
#[test]
fn a_query_with_no_page_order_has_nothing_to_advise() {
let mut q = invoices();
q.pagination_order = None;
assert_eq!(tie_break_key(&q), None);
assert!(advise(&q, "tb_invoice", &[index("ix", &["status"])]).is_empty());
}
#[test]
fn an_unindexed_tie_break_is_reported_with_its_ddl() {
let found = advise(&invoices(), "tb_invoice", &[]);
let first = found.first().expect("an unindexed tie-break is advice");
assert_eq!(
first.advice,
Advice::TieBreakUnindexed {
tie_break: "pk_invoice".to_string(),
}
);
assert_eq!(first.ddl, vec!["CREATE INDEX ON tb_invoice (pk_invoice);"]);
}
#[test]
fn a_tie_break_in_a_trailing_position_is_still_unindexed_for_the_default_order() {
let found = advise(&invoices(), "tb_invoice", &[index("ix", &["status", "pk_invoice"])]);
assert!(
found.iter().any(|a| matches!(a.advice, Advice::TieBreakUnindexed { .. })),
"a trailing tie-break does not serve the default ordering: {found:?}"
);
}
#[test]
fn an_indexed_sort_key_without_the_tie_break_is_reported() {
let indexes = [
index("ix_pk", &["pk_invoice"]),
index("ix_status", &["status"]),
];
let found = advise(&invoices(), "tb_invoice", &indexes);
let composite = found
.iter()
.find(|a| matches!(a.advice, Advice::MissingComposite { .. }))
.expect("an indexed sort key with no composite is advice");
assert_eq!(
composite.advice,
Advice::MissingComposite {
sort_keys: vec!["status".to_string()],
tie_break: "pk_invoice".to_string(),
}
);
assert_eq!(composite.ddl, vec!["CREATE INDEX ON tb_invoice (status, pk_invoice);"]);
assert!(
!found.iter().any(|a| matches!(a.advice, Advice::TieBreakUnindexed { .. })),
"the tie-break leads ix_pk, so it is not unindexed: {found:?}"
);
}
#[test]
fn the_composite_index_silences_both_findings() {
let indexes = [
index("ix_pk", &["pk_invoice"]),
index("ix_pair", &["status", "pk_invoice"]),
];
assert_eq!(advise(&invoices(), "tb_invoice", &indexes), Vec::new());
}
#[test]
fn an_unindexed_sort_key_is_not_this_passs_finding() {
let found = advise(&invoices(), "tb_invoice", &[index("ix_pk", &["pk_invoice"])]);
assert_eq!(found, Vec::new(), "{found:?}");
}
#[test]
fn a_query_without_order_by_gets_no_composite_advice() {
let mut q = invoices();
q.auto_params.has_order_by = false;
let found =
advise(&q, "tb_invoice", &[index("ix_pk", &["pk_invoice"]), index("ix_s", &["status"])]);
assert!(
!found.iter().any(|a| matches!(a.advice, Advice::MissingComposite { .. })),
"{found:?}"
);
}
fn identity_query() -> QueryDefinition {
let mut q = invoices();
q.pagination_order = Some(PaginationOrder::Identity);
q
}
#[test]
fn the_derived_identity_is_the_id_column() {
assert_eq!(tie_break_key(&identity_query()), Some("id".to_string()));
}
#[test]
fn an_index_on_the_id_column_serves_the_derived_identity() {
let found = advise(&identity_query(), "tb_invoice", &[index("ix_id", &["id"])]);
assert!(
!found.iter().any(|a| matches!(a.advice, Advice::TieBreakUnindexed { .. })),
"the id index already orders by the identity: {found:?}"
);
}
#[test]
fn a_missing_id_index_is_advised() {
let found = advise(&identity_query(), "tb_invoice", &[]);
let first = found.first().expect("advice");
assert_eq!(first.ddl, vec!["CREATE INDEX ON tb_invoice (id);"]);
}
#[test]
fn a_composite_over_the_identity_names_the_id_column_second() {
let found = advise(
&identity_query(),
"tb_invoice",
&[index("ix_id", &["id"]), index("ix_status", &["status"])],
);
let composite = found
.iter()
.find(|a| matches!(a.advice, Advice::MissingComposite { .. }))
.expect("advice");
assert_eq!(composite.ddl, vec!["CREATE INDEX ON tb_invoice (status, id);"]);
}
#[test]
fn the_sort_keys_are_reported_in_a_stable_order() {
let mut q = invoices();
for (key, pg_type) in [
("status", "text"),
("issuedAt", "timestamptz"),
("amount", "numeric"),
] {
q.native_columns
.insert(key.to_string(), fraiseql_core::schema::NativeColumn::nullable(pg_type));
}
let indexes = [
index("ix_pk", &["pk_invoice"]),
index("ix_status", &["status"]),
index("ix_issued", &["issued_at"]),
index("ix_amount", &["amount"]),
];
let Some(Advice::MissingComposite { sort_keys, .. }) =
advise(&q, "tb_invoice", &indexes).into_iter().map(|a| a.advice).next()
else {
panic!("expected composite advice");
};
assert_eq!(sort_keys, vec!["amount", "issued_at", "status"]);
}