use super::common::pgwire_harness::TestServer;
use super::helpers::{explain_lower, explain_raw};
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn create_index_on_strict_document_used_by_planner() {
let server = TestServer::start().await;
server
.exec(
"CREATE COLLECTION events (\
id STRING PRIMARY KEY, \
tenant_id STRING NOT NULL, \
user_id STRING NOT NULL, \
created_at TIMESTAMP) WITH (engine='document_strict')",
)
.await
.unwrap();
server
.exec("CREATE INDEX idx_events_lookup ON events(tenant_id)")
.await
.unwrap();
let plan = explain_lower(&server, "SELECT id FROM events WHERE tenant_id = 'acme'").await;
assert!(
plan.contains("indexedfetch")
|| plan.contains("indexlookup")
|| plan.contains("index lookup")
|| plan.contains("index_lookup"),
"plan for WHERE on indexed column must reference an index lookup, \
got: {plan}"
);
assert!(
!plan.contains("full scan") && !plan.contains("fulltablescan"),
"plan must not full-scan when indexed column has a CREATE INDEX, \
got: {plan}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn create_index_on_schemaless_document_used_by_planner() {
let server = TestServer::start().await;
server
.exec("CREATE COLLECTION idx_schemaless")
.await
.unwrap();
server
.exec("INSERT INTO idx_schemaless { id: 'a', role: 'admin' }")
.await
.unwrap();
server
.exec("CREATE INDEX ON idx_schemaless(role)")
.await
.unwrap();
let plan = explain_lower(
&server,
"SELECT id FROM idx_schemaless WHERE role = 'admin'",
)
.await;
assert!(
plan.contains("indexedfetch")
|| plan.contains("indexlookup")
|| plan.contains("index lookup")
|| plan.contains("index_lookup"),
"schemaless CREATE INDEX must wire into the planner the same way as \
a strict column index; got: {plan}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_index_removes_planner_awareness() {
let server = TestServer::start().await;
server.exec("CREATE COLLECTION idx_drop").await.unwrap();
server
.exec("CREATE INDEX idx_drop_role ON idx_drop(role)")
.await
.unwrap();
server.exec("DROP INDEX idx_drop_role").await.unwrap();
let plan = explain_lower(&server, "SELECT id FROM idx_drop WHERE role = 'admin'").await;
assert!(
!plan.contains("indexedfetch")
&& !plan.contains("indexlookup")
&& !plan.contains("index lookup")
&& !plan.contains("index_lookup"),
"after DROP INDEX the planner must not pick an index path, got: {plan}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn distinct_on_indexed_equality_uses_index() {
let server = TestServer::start().await;
server.exec("CREATE COLLECTION idx_distinct").await.unwrap();
server
.exec("CREATE INDEX ON idx_distinct(email)")
.await
.unwrap();
server
.exec("INSERT INTO idx_distinct { id: 'a', email: 'x@y.z' }")
.await
.unwrap();
let plan = explain_lower(
&server,
"SELECT DISTINCT id FROM idx_distinct WHERE email = 'x@y.z'",
)
.await;
assert!(
plan.contains("indexedfetch")
|| plan.contains("indexlookup")
|| plan.contains("index lookup")
|| plan.contains("index_lookup"),
"DISTINCT over an equality-on-indexed-field must still use the \
index path; got plan: {plan}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn indexed_fetch_plan_carries_projection() {
let server = TestServer::start().await;
server.exec("CREATE COLLECTION idx_proj").await.unwrap();
server
.exec("CREATE INDEX ON idx_proj(email)")
.await
.unwrap();
server
.exec("INSERT INTO idx_proj { id: 'a', email: 'x@y.z', role: 'admin' }")
.await
.unwrap();
let plan = explain_raw(&server, "SELECT id FROM idx_proj WHERE email = 'x@y.z'").await;
let lower = plan.to_lowercase();
assert!(
lower.contains("indexedfetch"),
"expected IndexedFetch in plan, got: {plan}"
);
assert!(
plan.contains("projection: [\"id\"]"),
"IndexedFetch plan must carry projection: [\"id\"], got: {plan}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn indexed_fetch_plan_carries_residual_filters() {
let server = TestServer::start().await;
server.exec("CREATE COLLECTION idx_resid").await.unwrap();
server
.exec("CREATE INDEX ON idx_resid(email)")
.await
.unwrap();
server
.exec(
"INSERT INTO idx_resid \
{ id: 'a', email: 'x@y.z', created_at: 100 }",
)
.await
.unwrap();
let plan = explain_raw(
&server,
"SELECT id FROM idx_resid \
WHERE email = 'x@y.z' AND created_at > 50",
)
.await;
let lower = plan.to_lowercase();
assert!(
lower.contains("indexedfetch"),
"expected IndexedFetch in plan, got: {plan}"
);
let marker = "filters: [";
let idx = plan
.find(marker)
.unwrap_or_else(|| panic!("no `filters:` field in plan: {plan}"));
let tail = &plan[idx + marker.len()..];
let close = tail
.find(']')
.unwrap_or_else(|| panic!("unterminated filters array in plan: {plan}"));
let filters_body = tail[..close].trim();
assert!(
!filters_body.is_empty(),
"IndexedFetch must carry residual `created_at > 50` in `filters`, \
not delegate to an outer Filter node — the Control-Plane post-filter \
compensation is the anti-pattern we're guarding against. Got plan: {plan}"
);
}