mod common;
use common::pgwire_harness::TestServer;
#[tokio::test]
async fn crdt_insert_and_select_round_trips() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('b', 't2', 'b2')")
.await
.unwrap();
let rows = srv
.query_rows("SELECT id, title FROM crdt_notes ORDER BY id")
.await
.unwrap();
let pairs: Vec<(&str, &str)> = rows
.iter()
.map(|r| (r[0].as_str(), r[1].as_str()))
.collect();
assert_eq!(
pairs,
vec![("a", "t1"), ("b", "t2")],
"CRDT INSERT must materialize into the readable store; got {pairs:?}"
);
}
#[tokio::test]
async fn crdt_update_set_merges_fields() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
srv.exec("UPDATE crdt_notes SET title='t2' WHERE id='a'")
.await
.unwrap();
let rows = srv
.query_rows("SELECT id, title, body FROM crdt_notes WHERE id='a'")
.await
.unwrap();
assert_eq!(
rows.len(),
1,
"exactly one row must remain after partial UPDATE; got {rows:?}"
);
assert_eq!(
rows[0][1].as_str(),
"t2",
"the touched field `title` must reflect the new value; got {:?}",
rows[0]
);
assert_eq!(
rows[0][2].as_str(),
"b1",
"the untouched field `body` must survive the partial UPDATE (LWW-per-field); got {:?}",
rows[0]
);
}
#[tokio::test]
async fn crdt_delete_removes_row() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
srv.exec("DELETE FROM crdt_notes WHERE id='a'")
.await
.unwrap();
let rows = srv
.query_rows("SELECT id FROM crdt_notes WHERE id='a'")
.await
.unwrap();
assert!(
rows.is_empty(),
"DELETE must remove the row from current-state reads; got {rows:?}"
);
}
#[tokio::test]
async fn crdt_upsert_replaces() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
srv.exec("UPSERT INTO crdt_notes (id, title, body) VALUES ('a', 't9', 'b9')")
.await
.unwrap();
let rows = srv
.query_rows("SELECT id, title, body FROM crdt_notes WHERE id='a'")
.await
.unwrap();
assert_eq!(
rows.len(),
1,
"UPSERT on an existing PK must not duplicate the row; got {rows:?}"
);
assert_eq!(
(rows[0][1].as_str(), rows[0][2].as_str()),
("t9", "b9"),
"UPSERT must replace all fields of the existing row; got {:?}",
rows[0]
);
}
#[tokio::test]
async fn predicate_update_on_crdt_rejected() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
srv.expect_error(
"UPDATE crdt_notes SET title='x' WHERE title='t1'",
"predicate (non-primary-key) UPDATE on CRDT collection",
)
.await;
}
#[tokio::test]
async fn predicate_delete_on_crdt_rejected() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
srv.expect_error(
"DELETE FROM crdt_notes WHERE title='t1'",
"predicate (non-primary-key) DELETE on CRDT collection",
)
.await;
}
#[tokio::test]
async fn crdt_update_returning_projects_updated_row() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
let rows = srv
.query_rows("UPDATE crdt_notes SET title='t2' WHERE id='a' RETURNING id, title")
.await
.unwrap();
assert_eq!(
rows.len(),
1,
"UPDATE ... RETURNING must project exactly the updated row; got {rows:?}"
);
assert_eq!(
(rows[0][0].as_str(), rows[0][1].as_str()),
("a", "t2"),
"UPDATE ... RETURNING must reflect the post-update field values; got {:?}",
rows[0]
);
}
#[tokio::test]
async fn crdt_delete_returning_projects_deleted_row() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE crdt_notes (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (crdt='true')",
)
.await
.unwrap();
srv.exec("INSERT INTO crdt_notes (id, title, body) VALUES ('a', 't1', 'b1')")
.await
.unwrap();
let rows = srv
.query_rows("DELETE FROM crdt_notes WHERE id='a' RETURNING id")
.await
.unwrap();
assert_eq!(
rows.len(),
1,
"DELETE ... RETURNING must project exactly the deleted row; got {rows:?}"
);
assert_eq!(
rows[0][0].as_str(),
"a",
"DELETE ... RETURNING must return the deleted row's id; got {:?}",
rows[0]
);
let remaining = srv
.query_rows("SELECT id FROM crdt_notes WHERE id='a'")
.await
.unwrap();
assert!(
remaining.is_empty(),
"DELETE ... RETURNING must still remove the row; got {remaining:?}"
);
}