use mongreldb_core::{
ColumnDef, ColumnFlags, Database, MongrelError, OwnedRow, RowId, Schema, Table, TypeId,
UpsertAction, UpsertActionKind, Value,
};
use tempfile::tempdir;
fn users_schema() -> Schema {
Schema {
schema_id: 1,
columns: vec![
ColumnDef {
id: 1,
name: "id".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::PRIMARY_KEY),
default_value: None,
},
ColumnDef {
id: 2,
name: "name".into(),
ty: TypeId::Bytes,
flags: ColumnFlags::empty(),
default_value: None,
},
],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
fn identity_schema() -> Schema {
Schema {
schema_id: 2,
columns: vec![
ColumnDef {
id: 1,
name: "id".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty()
.with(ColumnFlags::PRIMARY_KEY)
.with(ColumnFlags::AUTO_INCREMENT),
default_value: None,
},
ColumnDef {
id: 2,
name: "name".into(),
ty: TypeId::Bytes,
flags: ColumnFlags::empty(),
default_value: None,
},
],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
fn row(id: i64, name: &[u8]) -> Vec<(u16, Value)> {
vec![(1, Value::Int64(id)), (2, Value::Bytes(name.to_vec()))]
}
fn row_id_for(db: &Database, table: &str, id: i64) -> RowId {
db.table(table)
.unwrap()
.lock()
.lookup_pk(&Value::Int64(id).encode_key())
.unwrap()
}
fn cell(row: &OwnedRow, column_id: u16) -> Value {
row.columns
.iter()
.find(|(id, _)| *id == column_id)
.map(|(_, value)| value.clone())
.unwrap()
}
fn assert_conflict(err: MongrelError) {
assert!(
matches!(err, MongrelError::Conflict(_)),
"expected conflict, got {err:?}"
);
}
#[test]
fn put_returning_surfaces_auto_increment_post_image() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("users", identity_schema()).unwrap();
let mut tx = db.begin();
let first = tx
.put_returning("users", vec![(2, Value::Bytes(b"alice".to_vec()))])
.unwrap();
let second = tx
.put_returning("users", vec![(2, Value::Bytes(b"bob".to_vec()))])
.unwrap();
assert_eq!(first.auto_inc, Some(1));
assert_eq!(cell(&first.row, 1), Value::Int64(1));
assert_eq!(second.auto_inc, Some(2));
assert_eq!(cell(&second.row, 1), Value::Int64(2));
tx.commit().unwrap();
assert_eq!(db.table("users").unwrap().lock().count(), 2);
}
#[test]
fn upsert_do_nothing_and_update_survive_reopen() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
db.transaction(|tx| {
tx.put("users", row(1, b"alice"))?;
Ok(())
})
.unwrap();
let mut tx = db.begin();
let unchanged = tx
.upsert("users", row(1, b"ignored"), UpsertAction::DoNothing)
.unwrap();
assert_eq!(unchanged.action, UpsertActionKind::Unchanged);
assert_eq!(cell(&unchanged.row, 2), Value::Bytes(b"alice".to_vec()));
tx.commit().unwrap();
assert_eq!(db.table("users").unwrap().lock().count(), 1);
let mut tx = db.begin();
let updated = tx
.upsert(
"users",
row(1, b"ignored"),
UpsertAction::DoUpdate(vec![(2, Value::Bytes(b"ann".to_vec()))]),
)
.unwrap();
assert_eq!(updated.action, UpsertActionKind::Updated);
assert_eq!(cell(&updated.row, 2), Value::Bytes(b"ann".to_vec()));
tx.commit().unwrap();
let mut tx = db.begin();
let inserted = tx
.upsert("users", row(2, b"bob"), UpsertAction::DoNothing)
.unwrap();
assert_eq!(inserted.action, UpsertActionKind::Inserted);
tx.commit().unwrap();
}
let reopened = Database::open(dir.path()).unwrap();
let table = reopened.table("users").unwrap();
let guard = table.lock();
assert_eq!(guard.count(), 2);
let row = guard
.get(
guard.lookup_pk(&Value::Int64(1).encode_key()).unwrap(),
guard.snapshot(),
)
.unwrap();
assert_eq!(row.columns.get(&2), Some(&Value::Bytes(b"ann".to_vec())));
}
#[test]
fn upsert_do_update_identical_patch_is_unchanged() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
db.transaction(|tx| {
tx.put("users", row(1, b"alice"))?;
Ok(())
})
.unwrap();
let mut tx = db.begin();
let unchanged = tx
.upsert(
"users",
row(1, b"alice"),
UpsertAction::DoUpdate(vec![(2, Value::Bytes(b"alice".to_vec()))]),
)
.unwrap();
assert_eq!(unchanged.action, UpsertActionKind::Unchanged);
assert_eq!(cell(&unchanged.row, 2), Value::Bytes(b"alice".to_vec()));
tx.commit().unwrap();
let table = db.table("users").unwrap();
let guard = table.lock();
let r = guard
.get(
guard.lookup_pk(&Value::Int64(1).encode_key()).unwrap(),
guard.snapshot(),
)
.unwrap();
assert_eq!(r.columns.get(&2), Some(&Value::Bytes(b"alice".to_vec())));
}
#[test]
fn update_many_and_delete_many_return_images_and_survive_reopen() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
db.transaction(|tx| {
tx.put("users", row(1, b"alice"))?;
tx.put("users", row(2, b"bob"))?;
tx.put("users", row(3, b"cara"))?;
Ok(())
})
.unwrap();
let rid1 = row_id_for(&db, "users", 1);
let rid2 = row_id_for(&db, "users", 2);
let mut tx = db.begin();
let post = tx
.update_many(
"users",
vec![
(rid1, vec![(2, Value::Bytes(b"ann".to_vec()))]),
(rid2, vec![(2, Value::Bytes(b"ben".to_vec()))]),
],
)
.unwrap();
assert_eq!(post.len(), 2);
assert_eq!(cell(&post[0], 2), Value::Bytes(b"ann".to_vec()));
assert_eq!(cell(&post[1], 2), Value::Bytes(b"ben".to_vec()));
tx.commit().unwrap();
let rid2 = row_id_for(&db, "users", 2);
let rid3 = row_id_for(&db, "users", 3);
let mut tx = db.begin();
let deleted = tx.delete_many("users", vec![rid2, rid3]).unwrap();
assert_eq!(deleted.len(), 2);
assert_eq!(cell(&deleted[0], 2), Value::Bytes(b"ben".to_vec()));
assert_eq!(cell(&deleted[1], 2), Value::Bytes(b"cara".to_vec()));
tx.commit().unwrap();
assert_eq!(db.table("users").unwrap().lock().count(), 1);
}
let reopened = Database::open(dir.path()).unwrap();
assert_eq!(reopened.table("users").unwrap().lock().count(), 1);
}
#[test]
fn transaction_truncate_survives_reopen() {
let dir = tempdir().unwrap();
{
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
db.transaction(|tx| {
tx.put("users", row(1, b"alice"))?;
tx.put("users", row(2, b"bob"))?;
Ok(())
})
.unwrap();
assert_eq!(db.table("users").unwrap().lock().count(), 2);
db.transaction(|tx| tx.truncate("users")).unwrap();
assert_eq!(db.table("users").unwrap().lock().count(), 0);
}
let reopened = Database::open(dir.path()).unwrap();
assert_eq!(reopened.table("users").unwrap().lock().count(), 0);
}
#[test]
fn table_truncate_survives_reopen() {
let dir = tempdir().unwrap();
{
let table_dir = dir.path().join("table");
let mut table = Table::create(&table_dir, users_schema(), 1).unwrap();
table.put(row(1, b"alice")).unwrap();
table.put(row(2, b"bob")).unwrap();
table.commit().unwrap();
assert_eq!(table.count(), 2);
table.truncate().unwrap();
table.commit().unwrap();
assert_eq!(table.count(), 0);
}
let table = Table::open(dir.path().join("table")).unwrap();
assert_eq!(table.count(), 0);
}
#[test]
fn table_delete_returning_returns_pre_image() {
let dir = tempdir().unwrap();
let table_dir = dir.path().join("table");
let mut table = Table::create(&table_dir, users_schema(), 1).unwrap();
let row_id = table.put(row(1, b"alice")).unwrap();
table.commit().unwrap();
let pre = table.delete_returning(row_id).unwrap().unwrap();
assert_eq!(cell(&pre, 2), Value::Bytes(b"alice".to_vec()));
table.commit().unwrap();
assert_eq!(table.count(), 0);
}
#[test]
fn truncate_conflicts_with_concurrent_put_when_truncate_wins() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
db.transaction(|tx| {
tx.put("users", row(1, b"alice"))?;
Ok(())
})
.unwrap();
let mut truncate = db.begin();
let mut put = db.begin();
truncate.truncate("users").unwrap();
put.put("users", row(2, b"bob")).unwrap();
truncate.commit().unwrap();
assert_conflict(put.commit().unwrap_err());
}
#[test]
fn truncate_conflicts_with_concurrent_put_when_put_wins() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
db.transaction(|tx| {
tx.put("users", row(1, b"alice"))?;
Ok(())
})
.unwrap();
let mut truncate = db.begin();
let mut put = db.begin();
truncate.truncate("users").unwrap();
put.put("users", row(2, b"bob")).unwrap();
put.commit().unwrap();
assert_conflict(truncate.commit().unwrap_err());
}
#[test]
fn transaction_truncate_rejects_same_table_writes() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
let mut tx = db.begin();
tx.put("users", row(1, b"alice")).unwrap();
assert!(tx.truncate("users").is_err());
let mut tx = db.begin();
tx.truncate("users").unwrap();
assert!(tx.delete("users", RowId(1)).is_err());
}
#[test]
fn upsert_update_conflicts_with_concurrent_delete() {
let dir = tempdir().unwrap();
let db = Database::create(dir.path()).unwrap();
db.create_table("users", users_schema()).unwrap();
{
let t = db.table("users").unwrap();
let mut g = t.lock();
g.put(row(1, b"alice")).unwrap();
g.commit().unwrap();
}
{
let mut upsert = db.begin();
let mut delete = db.begin();
upsert
.upsert(
"users",
row(1, b"alice"),
UpsertAction::DoUpdate(vec![(2, Value::Bytes(b"updated".to_vec()))]),
)
.unwrap();
delete
.delete_many("users", vec![row_id_for(&db, "users", 1)])
.unwrap();
upsert.commit().unwrap();
assert_conflict(delete.commit().unwrap_err());
}
{
let t = db.table("users").unwrap();
let mut g = t.lock();
g.put(row(1, b"alice")).unwrap();
g.commit().unwrap();
}
{
let mut upsert = db.begin();
let mut delete = db.begin();
delete
.delete_many("users", vec![row_id_for(&db, "users", 1)])
.unwrap();
upsert
.upsert(
"users",
row(1, b"alice"),
UpsertAction::DoUpdate(vec![(2, Value::Bytes(b"updated2".to_vec()))]),
)
.unwrap();
delete.commit().unwrap();
assert_conflict(upsert.commit().unwrap_err());
}
}
#[test]
fn table_truncate_private_wal_deferred_until_commit() {
use mongreldb_core::manifest;
let dir = tempdir().unwrap();
let table_dir = dir.path().join("table");
{
let mut table = Table::create(&table_dir, users_schema(), 1).unwrap();
table.set_mutable_run_spill_bytes(1);
table.put(row(1, b"alice")).unwrap();
table.commit().unwrap();
table.flush().unwrap();
assert_eq!(table.count(), 1);
table.truncate().unwrap();
}
let table = Table::open(&table_dir).unwrap();
assert_eq!(table.count(), 1);
assert!(
table.lookup_pk(&Value::Int64(1).encode_key()).is_some(),
"row with pk=1 should survive an uncommitted truncate"
);
let manifest = manifest::read(&table_dir, None).unwrap();
assert!(
!manifest.runs.is_empty(),
"manifest should still reference the flushed run"
);
for run in &manifest.runs {
let path = table_dir.join("_runs").join(format!("r-{}.sr", run.run_id));
assert!(
path.exists(),
"run file referenced by manifest should still exist: {:?}",
path
);
}
}