mod common;
use common::pgwire_harness::TestServer;
async fn setup(server: &TestServer, coll: &str, storage_mode: &str) {
match storage_mode {
"document_strict" => {
server
.exec(&format!(
"CREATE COLLECTION {coll} \
(id STRING NOT NULL PRIMARY KEY, body STRING) \
WITH (engine='document_strict')"
))
.await
.unwrap();
}
_ => {
server
.exec(&format!(
"CREATE COLLECTION {coll} WITH (engine='document_schemaless')"
))
.await
.unwrap();
}
}
for (id, body) in [
("a1", "the quick brown fox"),
("a2", "a lazy dog sleeps"),
("unrelated", "completely different topic"),
] {
server
.exec(&format!(
"INSERT INTO {coll} (id, body) VALUES ('{id}', '{body}')"
))
.await
.unwrap();
}
}
async fn matched_ids(server: &TestServer, coll: &str, term: &str) -> Vec<String> {
let rows = server
.query_rows(&format!(
"SELECT id FROM {coll} WHERE text_match(body, '{term}') ORDER BY id"
))
.await
.unwrap();
rows.into_iter().map(|r| r[0].clone()).collect()
}
async fn insert_visible_in_txn_then_commit(engine: &str, coll: &str) {
let server = TestServer::start().await;
setup(&server, coll, engine).await;
let base = matched_ids(&server, coll, "elephant").await;
assert!(base.is_empty(), "{engine}: baseline must not match yet");
server.exec("BEGIN").await.unwrap();
server
.exec(&format!(
"INSERT INTO {coll} (id, body) VALUES ('new1', 'an elephant never forgets')"
))
.await
.unwrap();
let in_txn = matched_ids(&server, coll, "elephant").await;
assert_eq!(
in_txn,
vec!["new1".to_string()],
"{engine}: in-tx search must include the staged insert before COMMIT"
);
server.client.simple_query("COMMIT").await.unwrap();
let after_commit = matched_ids(&server, coll, "elephant").await;
assert_eq!(
after_commit,
vec!["new1".to_string()],
"{engine}: committed insert stays visible to search"
);
}
async fn insert_visible_in_txn_then_rollback(engine: &str, coll: &str) {
let server = TestServer::start().await;
setup(&server, coll, engine).await;
server.exec("BEGIN").await.unwrap();
server
.exec(&format!(
"INSERT INTO {coll} (id, body) VALUES ('new2', 'a giraffe is very tall')"
))
.await
.unwrap();
let in_txn = matched_ids(&server, coll, "giraffe").await;
assert_eq!(
in_txn,
vec!["new2".to_string()],
"{engine}: in-tx search must include the staged insert"
);
server.client.simple_query("ROLLBACK").await.unwrap();
let after_rollback = matched_ids(&server, coll, "giraffe").await;
assert!(
after_rollback.is_empty(),
"{engine}: ROLLBACK must leave no durable index trace: {after_rollback:?}"
);
}
async fn delete_hides_in_txn_then_rollback_restores(engine: &str, coll: &str) {
let server = TestServer::start().await;
setup(&server, coll, engine).await;
let base = matched_ids(&server, coll, "fox").await;
assert_eq!(base, vec!["a1".to_string()], "{engine}: baseline match");
server.exec("BEGIN").await.unwrap();
server
.exec(&format!("DELETE FROM {coll} WHERE id = 'a1'"))
.await
.unwrap();
let in_txn = matched_ids(&server, coll, "fox").await;
assert!(
in_txn.is_empty(),
"{engine}: in-tx search must exclude the staged delete: {in_txn:?}"
);
server.client.simple_query("ROLLBACK").await.unwrap();
let after_rollback = matched_ids(&server, coll, "fox").await;
assert_eq!(
after_rollback,
vec!["a1".to_string()],
"{engine}: ROLLBACK restores the deleted doc's match"
);
}
async fn update_changes_match_in_txn(engine: &str, coll: &str) {
let server = TestServer::start().await;
setup(&server, coll, engine).await;
server.exec("BEGIN").await.unwrap();
server
.exec(&format!(
"UPDATE {coll} SET body = 'nothing to see here' WHERE id = 'a1'"
))
.await
.unwrap();
server
.exec(&format!(
"UPDATE {coll} SET body = 'a fox in the henhouse' WHERE id = 'a2'"
))
.await
.unwrap();
let in_txn = matched_ids(&server, coll, "fox").await;
assert_eq!(
in_txn,
vec!["a2".to_string()],
"{engine}: in-tx search must reflect both the moved-out and moved-in update"
);
server.client.simple_query("ROLLBACK").await.unwrap();
let after_rollback = matched_ids(&server, coll, "fox").await;
assert_eq!(
after_rollback,
vec!["a1".to_string()],
"{engine}: ROLLBACK restores base match state"
);
}
async fn unrelated_docs_unaffected(engine: &str, coll: &str) {
let server = TestServer::start().await;
setup(&server, coll, engine).await;
server.exec("BEGIN").await.unwrap();
server
.exec(&format!(
"INSERT INTO {coll} (id, body) VALUES ('new3', 'brown fox and a quick dog')"
))
.await
.unwrap();
let mut in_txn = matched_ids(&server, coll, "dog").await;
in_txn.sort();
assert_eq!(
in_txn,
vec!["a2".to_string(), "new3".to_string()],
"{engine}: base match for 'a2' must be unaffected by an unrelated staged insert"
);
let unrelated = matched_ids(&server, coll, "kangaroo").await;
assert!(unrelated.is_empty());
server.client.simple_query("ROLLBACK").await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn schemaless_insert_visible_in_txn_then_commit() {
insert_visible_in_txn_then_commit("document_schemaless", "fts_ov_ins_c").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn schemaless_insert_visible_in_txn_then_rollback() {
insert_visible_in_txn_then_rollback("document_schemaless", "fts_ov_ins_r").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn schemaless_delete_hides_in_txn_then_rollback_restores() {
delete_hides_in_txn_then_rollback_restores("document_schemaless", "fts_ov_del").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn schemaless_update_changes_match_in_txn() {
update_changes_match_in_txn("document_schemaless", "fts_ov_upd").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn schemaless_unrelated_docs_unaffected() {
unrelated_docs_unaffected("document_schemaless", "fts_ov_unrel").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn strict_insert_visible_in_txn_then_commit() {
insert_visible_in_txn_then_commit("document_strict", "fts_ov_st_ins_c").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn strict_insert_visible_in_txn_then_rollback() {
insert_visible_in_txn_then_rollback("document_strict", "fts_ov_st_ins_r").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn strict_delete_hides_in_txn_then_rollback_restores() {
delete_hides_in_txn_then_rollback_restores("document_strict", "fts_ov_st_del").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn strict_update_changes_match_in_txn() {
update_changes_match_in_txn("document_strict", "fts_ov_st_upd").await;
}