use mongreldb_core::query::{Condition, Query};
use mongreldb_core::schema::{ColumnDef, ColumnFlags, IndexDef, IndexKind, Schema, TypeId};
use mongreldb_core::{RowId, Table, Value};
use tempfile::tempdir;
fn base_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: "v".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty(),
default_value: None,
},
],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
fn schema_with_bitmap() -> 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: "tag".into(),
ty: TypeId::Bytes,
flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
default_value: None,
},
ColumnDef {
id: 3,
name: "score".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
default_value: None,
},
],
indexes: vec![IndexDef {
name: "tag_bm".into(),
column_id: 2,
kind: IndexKind::Bitmap,
predicate: None,
}],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
fn put2(t: &mut Table, id: i64, v: i64) -> RowId {
t.put(vec![(1, Value::Int64(id)), (2, Value::Int64(v))])
.unwrap()
}
fn put3(t: &mut Table, id: i64, tag: &[u8], score: i64) -> RowId {
t.put(vec![
(1, Value::Int64(id)),
(2, Value::Bytes(tag.to_vec())),
(3, Value::Int64(score)),
])
.unwrap()
}
fn snapshot_row_ids(t: &Table) -> Vec<u64> {
let snap = t.snapshot();
t.visible_rows(snap)
.unwrap()
.into_iter()
.map(|r| r.row_id.0)
.collect()
}
fn snapshot_values(t: &Table) -> Vec<(u64, i64)> {
let snap = t.snapshot();
let mut rows: Vec<(u64, i64)> = t
.visible_rows(snap)
.unwrap()
.into_iter()
.map(|r| {
let v = match r.columns.get(&1) {
Some(Value::Int64(n)) => *n,
_ => panic!("expected int id"),
};
(r.row_id.0, v)
})
.collect();
rows.sort_by_key(|x| x.0);
rows
}
#[test]
fn uncommitted_rows_are_invisible() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
assert_eq!(t.count(), 1, "live_count tracks uncommitted puts");
assert_eq!(
snapshot_row_ids(&t).len(),
0,
"uncommitted row must not be visible"
);
t.commit().unwrap();
assert_eq!(snapshot_row_ids(&t).len(), 1, "committed row visible");
}
#[test]
fn data_survives_close_open_without_flush() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
assert_eq!(t.count(), 2);
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 2);
let rows = snapshot_values(&t);
assert_eq!(rows, vec![(0, 1), (1, 2)]);
}
}
#[test]
fn data_survives_close_open_with_flush() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
t.flush().unwrap();
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 2);
let rows = snapshot_values(&t);
assert_eq!(rows, vec![(0, 1), (1, 2)]);
}
}
#[test]
fn delete_then_reopen_without_flush_hides_row() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let a = put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
t.delete(a).unwrap();
t.commit().unwrap();
assert_eq!(t.count(), 1);
drop(t);
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 1);
let visible = snapshot_row_ids(&t);
assert_eq!(visible.len(), 1);
assert!(
!visible.contains(&a.0),
"deleted row must stay hidden after reopen"
);
}
#[test]
fn delete_then_reopen_after_flush_hides_row() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let a = put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
t.delete(a).unwrap();
t.flush().unwrap();
drop(t);
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 1);
let visible = snapshot_row_ids(&t);
assert_eq!(visible.len(), 1);
assert!(!visible.contains(&a.0));
}
#[test]
fn count_matches_visible_rows_after_mixed_operations() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
assert_eq!(t.count(), 0);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 0);
let a = put2(&mut t, 1, 10);
let b = put2(&mut t, 2, 20);
let c = put2(&mut t, 3, 30);
t.commit().unwrap();
assert_eq!(t.count(), 3);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 3);
t.delete(b).unwrap();
t.commit().unwrap();
assert_eq!(t.count(), 2);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 2);
put2(&mut t, 4, 40);
t.commit().unwrap();
assert_eq!(t.count(), 3);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 3);
t.flush().unwrap();
assert_eq!(
t.count(),
t.visible_rows(t.snapshot()).unwrap().len() as u64
);
drop(t);
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 3);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 3);
let ids = snapshot_row_ids(&t);
assert!(ids.contains(&a.0));
assert!(ids.contains(&c.0));
}
#[test]
fn bulk_load_and_individual_puts_interleaved_survive_reopen() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = (0..500)
.map(|i| vec![(1, Value::Int64(i)), (2, Value::Int64(i * 10))])
.collect();
t.bulk_load(batch).unwrap();
for i in 500..600 {
put2(&mut t, i, i * 10);
}
t.commit().unwrap();
for i in 600..650 {
put2(&mut t, i, i * 10);
}
t.commit().unwrap();
assert_eq!(t.count(), 650);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 650);
assert_eq!(rows[0], (0, 0));
assert_eq!(rows[499], (499, 499));
assert_eq!(rows[649], (649, 649));
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 650);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 650);
assert_eq!(rows[649], (649, 649));
}
}
#[test]
fn flush_after_interleaved_writes_is_durable() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = (0..200)
.map(|i| vec![(1, Value::Int64(i)), (2, Value::Int64(i * 2))])
.collect();
t.bulk_load(batch).unwrap();
for i in 200..250 {
put2(&mut t, i, i * 2);
}
t.commit().unwrap();
t.flush().unwrap();
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 250);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 250);
assert_eq!(rows.last().copied(), Some((249, 249)));
}
}
#[test]
fn schema_evolution_add_column_reads_null_for_old_rows() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
t.flush().unwrap();
let new_cid = t
.add_column(
"extra",
TypeId::Int64,
ColumnFlags::empty().with(ColumnFlags::NULLABLE),
None,
)
.unwrap();
assert_eq!(new_cid, 3);
t.put(vec![
(1, Value::Int64(3)),
(2, Value::Int64(30)),
(3, Value::Int64(300)),
])
.unwrap();
t.commit().unwrap();
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 3);
let snap = t.snapshot();
let cols = t.visible_columns(snap).unwrap();
let id_col: Vec<Value> = cols.iter().find(|(id, _)| *id == 1).unwrap().1.clone();
let extra_col: Vec<Value> = cols.iter().find(|(id, _)| *id == 3).unwrap().1.clone();
assert_eq!(id_col.len(), 3);
assert_eq!(extra_col.len(), 3);
for (i, id_val) in id_col.iter().enumerate() {
match id_val {
Value::Int64(1) | Value::Int64(2) => {
assert_eq!(
extra_col[i],
Value::Null,
"old row must have null new column"
);
}
Value::Int64(3) => {
assert_eq!(extra_col[i], Value::Int64(300));
}
_ => panic!("unexpected row"),
}
}
}
}
#[test]
fn schema_evolution_query_on_new_column_does_not_crash() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
t.flush().unwrap();
let _ = t
.add_column(
"extra",
TypeId::Int64,
ColumnFlags::empty().with(ColumnFlags::NULLABLE),
None,
)
.unwrap();
let q = Query::new().and(Condition::Range {
column_id: 3,
lo: 0,
hi: 1000,
});
let rows = t.query(&q).unwrap();
assert_eq!(
rows.len(),
0,
"old rows have null new column, should not match range"
);
}
#[test]
fn partial_column_puts_store_nulls() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), schema_with_bitmap(), 1).unwrap();
put3(&mut t, 1, b"a", 10);
t.put(vec![(1, Value::Int64(2)), (2, Value::Bytes(b"b".to_vec()))])
.unwrap();
t.put(vec![(1, Value::Int64(3)), (3, Value::Int64(30))])
.unwrap();
t.commit().unwrap();
let snap = t.snapshot();
let rows = t.visible_rows(snap).unwrap();
assert_eq!(rows.len(), 3);
let by_id: std::collections::HashMap<u64, &mongreldb_core::memtable::Row> =
rows.iter().map(|r| (r.row_id.0, r)).collect();
let r1 = by_id[&0];
assert_eq!(r1.columns.get(&2), Some(&Value::Bytes(b"a".to_vec())));
assert_eq!(r1.columns.get(&3), Some(&Value::Int64(10)));
let r2 = by_id[&1];
assert_eq!(r2.columns.get(&2), Some(&Value::Bytes(b"b".to_vec())));
assert_eq!(r2.columns.get(&3), None);
let r3 = by_id[&2];
assert_eq!(r3.columns.get(&2), None);
assert_eq!(r3.columns.get(&3), Some(&Value::Int64(30)));
let cols = t.visible_columns(snap).unwrap();
let col2: Vec<Value> = cols.iter().find(|(id, _)| *id == 2).unwrap().1.clone();
let col3: Vec<Value> = cols.iter().find(|(id, _)| *id == 3).unwrap().1.clone();
assert_eq!(
col2,
vec![
Value::Bytes(b"a".to_vec()),
Value::Bytes(b"b".to_vec()),
Value::Null,
]
);
assert_eq!(col3, vec![Value::Int64(10), Value::Null, Value::Int64(30)]);
}
fn schema_with_not_null() -> 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: "req".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty(),
default_value: None,
},
ColumnDef {
id: 3,
name: "opt".into(),
ty: TypeId::Int64,
flags: ColumnFlags::empty().with(ColumnFlags::NULLABLE),
default_value: None,
},
],
indexes: vec![],
colocation: vec![],
constraints: Default::default(),
clustered: false,
}
}
#[test]
fn not_null_column_rejects_omit_and_explicit_null() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), schema_with_not_null(), 1).unwrap();
assert!(t.put(vec![(1, Value::Int64(1))]).is_err());
assert!(t.put(vec![(1, Value::Int64(2)), (2, Value::Null)]).is_err());
t.put(vec![(1, Value::Int64(3)), (2, Value::Int64(30))])
.unwrap();
t.put(vec![
(1, Value::Int64(4)),
(2, Value::Int64(40)),
(3, Value::Null),
])
.unwrap();
t.commit().unwrap();
assert_eq!(t.count(), 2);
}
#[test]
fn not_null_enforced_in_put_batch() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), schema_with_not_null(), 1).unwrap();
let batch = vec![
vec![(1, Value::Int64(1)), (2, Value::Int64(10))],
vec![(1, Value::Int64(2))], ];
assert!(t.put_batch(batch).is_err());
t.commit().unwrap();
assert_eq!(t.count(), 0);
}
#[test]
fn duplicate_pk_upserts_to_single_visible_row() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let first = put2(&mut t, 42, 100);
let second = put2(&mut t, 42, 200); t.commit().unwrap();
assert_eq!(t.count(), 1, "duplicate PK must not create two live rows");
assert_eq!(
t.visible_rows(t.snapshot()).unwrap().len(),
1,
"only the upserted row is visible"
);
let pk_lookup = t.lookup_pk(&42i64.to_be_bytes());
assert_eq!(
pk_lookup,
Some(second),
"PK lookup returns the latest row id"
);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, second.0);
assert!(!rows.iter().any(|(rid, _)| *rid == first.0));
drop(t);
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 1);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 1);
assert_eq!(t.lookup_pk(&42i64.to_be_bytes()), Some(second));
}
#[test]
fn duplicate_pk_across_commits_upserts() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let first = put2(&mut t, 7, 100);
t.commit().unwrap();
let second = put2(&mut t, 7, 200);
t.commit().unwrap();
assert_eq!(t.count(), 1);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 1);
assert_eq!(t.lookup_pk(&7i64.to_be_bytes()), Some(second));
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, second.0);
assert!(!rows.iter().any(|(rid, _)| *rid == first.0));
}
#[test]
fn duplicate_pk_query_returns_only_latest() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 42, 100);
put2(&mut t, 42, 200);
t.commit().unwrap();
let q = Query::pk(42i64.to_be_bytes().to_vec());
let rows = t.query(&q).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].columns.get(&2), Some(&Value::Int64(200)));
}
#[test]
fn visible_rows_respects_snapshot_epoch() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
t.commit().unwrap();
let snap1 = t.pin_snapshot();
put2(&mut t, 2, 20);
t.commit().unwrap();
let snap2 = t.pin_snapshot();
put2(&mut t, 3, 30);
t.commit().unwrap();
let rows1 = t.visible_rows(snap1).unwrap();
let rows2 = t.visible_rows(snap2).unwrap();
let rows_now = t.visible_rows(t.snapshot()).unwrap();
assert_eq!(rows1.len(), 1, "snapshot1 sees only first row");
assert_eq!(rows2.len(), 2, "snapshot2 sees first two rows");
assert_eq!(rows_now.len(), 3, "current snapshot sees all rows");
t.unpin_snapshot(snap1);
t.unpin_snapshot(snap2);
}
#[test]
fn row_ids_are_monotonic_after_delete() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let a = put2(&mut t, 1, 10);
let b = put2(&mut t, 2, 20);
t.commit().unwrap();
t.delete(a).unwrap();
t.commit().unwrap();
let c = put2(&mut t, 3, 30);
t.commit().unwrap();
assert!(
c.0 > b.0,
"new row id must exceed all previously allocated ids"
);
assert_eq!(t.count(), 2);
let ids = snapshot_row_ids(&t);
assert!(!ids.contains(&a.0));
assert!(ids.contains(&b.0));
assert!(ids.contains(&c.0));
}
#[test]
fn update_via_delete_and_put_preserves_single_pk_row() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let old = put2(&mut t, 1, 10);
t.commit().unwrap();
t.delete(old).unwrap();
let new = put2(&mut t, 1, 99);
t.commit().unwrap();
assert_eq!(t.count(), 1);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].0, new.0);
drop(t);
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 1);
assert_eq!(t.lookup_pk(&1i64.to_be_bytes()), Some(new));
}
#[test]
fn reopen_after_add_column_without_flush() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
t.commit().unwrap();
let _ = t
.add_column(
"extra",
TypeId::Bytes,
ColumnFlags::empty().with(ColumnFlags::NULLABLE),
None,
)
.unwrap();
put2(&mut t, 2, 20);
t.commit().unwrap();
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 2);
let snap = t.snapshot();
let cols = t.visible_columns(snap).unwrap();
let id_col: Vec<Value> = cols.iter().find(|(id, _)| *id == 1).unwrap().1.clone();
let extra_col: Vec<Value> = cols.iter().find(|(id, _)| *id == 3).unwrap().1.clone();
assert_eq!(id_col.len(), 2);
for (i, id_val) in id_col.iter().enumerate() {
if id_val == &Value::Int64(1) {
assert_eq!(
extra_col[i],
Value::Null,
"old row must read null new column"
);
}
}
}
}
#[test]
fn many_small_commits_survive_reopen() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
for i in 0..100i64 {
put2(&mut t, i, i * 2);
t.commit().unwrap();
}
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 100);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 100);
for (i, (rid, id)) in rows.iter().enumerate() {
assert_eq!(*rid, i as u64);
assert_eq!(*id, i as i64);
}
}
}
#[test]
fn put_batch_and_individual_puts_together() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = (0..50)
.map(|i| vec![(1, Value::Int64(i)), (2, Value::Int64(i))])
.collect();
t.put_batch(batch).unwrap();
put2(&mut t, 50, 50);
t.commit().unwrap();
assert_eq!(t.count(), 51);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 51);
assert_eq!(rows[50], (50, 50));
}
#[test]
fn bulk_load_then_put_then_flush_small_spill_threshold() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = (0..100)
.map(|i| vec![(1, Value::Int64(i)), (2, Value::Int64(i))])
.collect();
t.bulk_load(batch).unwrap();
t.set_mutable_run_spill_bytes(1);
for i in 100..120 {
put2(&mut t, i, i);
}
t.flush().unwrap();
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 120);
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 120);
assert_eq!(rows[119], (119, 119));
}
}
#[test]
fn reopen_empty_table_is_valid() {
let dir = tempdir().unwrap();
{
let _ = Table::create(dir.path(), base_schema(), 1).unwrap();
}
{
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 0);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 0);
}
}
#[test]
fn add_column_does_not_change_count() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
put2(&mut t, 1, 10);
put2(&mut t, 2, 20);
t.commit().unwrap();
assert_eq!(t.count(), 2);
let _ = t
.add_column(
"extra",
TypeId::Int64,
ColumnFlags::empty().with(ColumnFlags::NULLABLE),
None,
)
.unwrap();
assert_eq!(t.count(), 2, "add_column must not change live row count");
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 2);
t.flush().unwrap();
assert_eq!(t.count(), 2);
}
#[test]
fn partial_put_not_indexed_for_bitmap_equality() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), schema_with_bitmap(), 1).unwrap();
put3(&mut t, 1, b"x", 0);
put3(&mut t, 2, b"y", 0);
t.put(vec![(1, Value::Int64(3)), (3, Value::Int64(0))])
.unwrap();
t.commit().unwrap();
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 3);
let q = Query::new().and(Condition::BitmapEq {
column_id: 2,
value: b"x".to_vec(),
});
let rows = t.query(&q).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].columns.get(&1), Some(&Value::Int64(1)));
let qz = Query::new().and(Condition::BitmapEq {
column_id: 2,
value: b"z".to_vec(),
});
assert_eq!(t.query(&qz).unwrap().len(), 0);
}
#[test]
fn repeated_delete_and_put_cycles_keep_count_correct() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let mut handles = Vec::new();
for i in 0..20i64 {
let rid = put2(&mut t, i, i * 10);
handles.push(rid);
}
t.commit().unwrap();
for (i, rid) in handles.iter().enumerate().step_by(2) {
t.delete(*rid).unwrap();
put2(&mut t, 1000 + i as i64, i as i64 * 100);
}
t.commit().unwrap();
assert_eq!(t.count(), 20);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 20);
t.flush().unwrap();
assert_eq!(
t.count(),
t.visible_rows(t.snapshot()).unwrap().len() as u64
);
drop(t);
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 20);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 20);
}
#[test]
fn query_results_preserved_after_reopen() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), schema_with_bitmap(), 1).unwrap();
for i in 0..50i64 {
put3(&mut t, i, b"tok", i);
}
t.commit().unwrap();
t.flush().unwrap();
let q = Query::new().and(Condition::BitmapEq {
column_id: 2,
value: b"tok".to_vec(),
});
assert_eq!(t.query(&q).unwrap().len(), 50);
}
{
let mut t = Table::open(dir.path()).unwrap();
let q = Query::new().and(Condition::BitmapEq {
column_id: 2,
value: b"tok".to_vec(),
});
let rows = t.query(&q).unwrap();
assert_eq!(rows.len(), 50, "query result must survive reopen");
let ids: Vec<i64> = rows
.iter()
.map(|r| match r.columns.get(&1) {
Some(Value::Int64(n)) => *n,
_ => panic!("expected int"),
})
.collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..50).collect::<Vec<i64>>());
}
}
#[test]
fn duplicate_pk_inside_put_batch_upserts() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch = vec![
vec![(1, Value::Int64(5)), (2, Value::Int64(100))],
vec![(1, Value::Int64(5)), (2, Value::Int64(200))],
];
let ids = t.put_batch(batch).unwrap();
t.commit().unwrap();
assert_eq!(t.count(), 1, "duplicate PK inside put_batch must upsert");
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 1);
assert_eq!(t.lookup_pk(&5i64.to_be_bytes()), Some(ids[1]));
let rows = t.visible_rows(t.snapshot()).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].columns.get(&2), Some(&Value::Int64(200)));
}
#[test]
fn bulk_load_then_put_same_pk_upserts() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = (0..100)
.map(|i| vec![(1, Value::Int64(i)), (2, Value::Int64(i * 10))])
.collect();
t.bulk_load(batch).unwrap();
let updated = put2(&mut t, 50, 999);
t.commit().unwrap();
assert_eq!(t.count(), 100, "bulk load + put must keep 100 visible rows");
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 100);
assert_eq!(t.lookup_pk(&50i64.to_be_bytes()), Some(updated));
let rows = snapshot_values(&t);
assert_eq!(rows.len(), 100);
let row50 = rows.iter().find(|(_, id)| *id == 50).copied().unwrap();
assert_eq!(row50.0, updated.0);
}
#[test]
fn delete_then_flush_then_reopen_hot_consistent() {
let dir = tempdir().unwrap();
let rid = {
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let rid = put2(&mut t, 1, 10);
t.commit().unwrap();
t.delete(rid).unwrap();
t.commit().unwrap();
t.flush().unwrap();
rid
};
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 0);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 0);
assert_eq!(t.lookup_pk(&1i64.to_be_bytes()), None);
let mut t = t;
let rid2 = put2(&mut t, 1, 20);
t.commit().unwrap();
assert_eq!(t.count(), 1);
assert_eq!(t.lookup_pk(&1i64.to_be_bytes()), Some(rid2));
assert_ne!(rid2.0, rid.0);
}
#[test]
fn delete_then_spill_then_reopen_with_invalid_checkpoint_hot_clean() {
let dir = tempdir().unwrap();
let rid = {
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
t.set_mutable_run_spill_bytes(1);
let rid = put2(&mut t, 1, 10);
t.commit().unwrap();
t.delete(rid).unwrap();
t.commit().unwrap();
t.flush().unwrap();
rid
};
let _ = std::fs::remove_dir_all(dir.path().join("_idx"));
let t = Table::open(dir.path()).unwrap();
assert_eq!(t.count(), 0);
assert_eq!(t.visible_rows(t.snapshot()).unwrap().len(), 0);
assert_eq!(t.lookup_pk(&1i64.to_be_bytes()), None);
let mut t = t;
let rid2 = put2(&mut t, 1, 20);
t.commit().unwrap();
assert_eq!(t.count(), 1);
assert_eq!(t.lookup_pk(&1i64.to_be_bytes()), Some(rid2));
assert_ne!(rid2.0, rid.0);
}
#[test]
fn bulk_load_duplicate_pk_upserts_within_batch() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = vec![
vec![(1, Value::Int64(7)), (2, Value::Int64(100))],
vec![(1, Value::Int64(8)), (2, Value::Int64(200))],
vec![(1, Value::Int64(7)), (2, Value::Int64(999))], ];
t.bulk_load(batch).unwrap();
assert_eq!(t.count(), 2, "duplicate PKs in a bulk load must upsert");
let rows = t.visible_rows(t.snapshot()).unwrap();
assert_eq!(rows.len(), 2, "scan must also see only the winners");
let row7 = rows
.iter()
.find(|r| r.columns.get(&1) == Some(&Value::Int64(7)))
.unwrap();
assert_eq!(
row7.columns.get(&2),
Some(&Value::Int64(999)),
"last duplicate PK value must win"
);
}
#[test]
fn bulk_load_duplicate_pk_survives_reopen() {
let dir = tempdir().unwrap();
{
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = vec![
vec![(1, Value::Int64(7)), (2, Value::Int64(100))],
vec![(1, Value::Int64(7)), (2, Value::Int64(200))],
];
t.bulk_load(batch).unwrap();
assert_eq!(t.count(), 1);
}
let t = Table::open(dir.path()).unwrap();
assert_eq!(
t.count(),
1,
"bulk-load PK dedup must persist across reopen"
);
let rows = t.visible_rows(t.snapshot()).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].columns.get(&2), Some(&Value::Int64(200)));
assert_eq!(
t.lookup_pk(&7i64.to_be_bytes()),
Some(rows[0].row_id),
"reopened HOT index must point at the surviving winner"
);
}
#[test]
fn bulk_load_rejects_missing_not_null_column() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = vec![vec![(1, Value::Int64(7))]];
let res = t.bulk_load(batch);
assert!(
res.is_err(),
"bulk_load must reject a batch missing a NOT NULL column"
);
assert_eq!(t.count(), 0, "rejected bulk load must not change count");
}
#[test]
fn bulk_load_rejects_explicit_null_in_not_null_column() {
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let batch: Vec<Vec<(u16, Value)>> = vec![vec![(1, Value::Int64(7)), (2, Value::Null)]];
assert!(t.bulk_load(batch).is_err());
assert_eq!(t.count(), 0);
}
#[test]
fn bulk_load_columns_duplicate_pk_upserts() {
use mongreldb_core::columnar::NativeColumn;
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let pk = NativeColumn::Int64 {
data: vec![7, 8, 7],
validity: vec![0b111],
};
let v = NativeColumn::Int64 {
data: vec![100, 200, 999],
validity: vec![0b111],
};
t.bulk_load_columns(vec![(1, pk), (2, v)]).unwrap();
assert_eq!(t.count(), 2);
let rows = t.visible_rows(t.snapshot()).unwrap();
assert_eq!(rows.len(), 2);
let row7 = rows
.iter()
.find(|r| r.columns.get(&1) == Some(&Value::Int64(7)))
.unwrap();
assert_eq!(row7.columns.get(&2), Some(&Value::Int64(999)));
}
#[test]
fn bulk_load_fast_rejects_not_null_violation() {
use mongreldb_core::columnar::NativeColumn;
let dir = tempdir().unwrap();
let mut t = Table::create(dir.path(), base_schema(), 1).unwrap();
let pk = NativeColumn::Int64 {
data: vec![7],
validity: vec![0b1],
};
let v = NativeColumn::Int64 {
data: vec![0],
validity: vec![0b0], };
let res = t.bulk_load_fast(vec![(1, pk), (2, v)]);
assert!(res.is_err(), "bulk_load_fast must enforce NOT NULL");
assert_eq!(t.count(), 0);
}