#![recursion_limit = "512"]
mod helpers;
use omnigraph::db::{MergeOutcome, Omnigraph};
use omnigraph::instrumentation::{MergeWriteProbes, with_merge_write_probes};
use helpers::*;
async fn append_new_persons(db: &mut Omnigraph, branch: &str, n: usize) {
for i in 0..n {
mutate_branch(
db,
branch,
MUTATION_QUERIES,
"insert_person",
&mixed_params(&[("$name", &format!("ff_new_{i}"))], &[("$age", 30)]),
)
.await
.unwrap();
}
}
#[tokio::test]
async fn append_only_fast_forward_merge_does_no_merge_insert() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let main = init_and_load(&dir).await;
main.branch_create("feature").await.unwrap();
let mut feature = Omnigraph::open(uri).await.unwrap();
append_new_persons(&mut feature, "feature", 5).await;
let probes = MergeWriteProbes::default();
let outcome =
with_merge_write_probes(probes.clone(), main.branch_merge("feature", "main"))
.await
.unwrap();
assert_eq!(outcome, MergeOutcome::FastForward);
assert_eq!(
probes.stage_merge_insert_calls(),
0,
"append-only fast-forward merge must do 0 stage_merge_insert (the OOM hash join); did {}",
probes.stage_merge_insert_calls(),
);
assert!(
probes.stage_append_calls() >= 1,
"append-only fast-forward merge must append the new rows via stage_append; did {}",
probes.stage_append_calls(),
);
assert_eq!(
probes.scan_staged_combined_calls(),
0,
"append-only merge must stream the append (stage_append_stream), not materialize the \
whole delta into one batch via scan_staged_combined; did {}",
probes.scan_staged_combined_calls(),
);
}
#[tokio::test]
async fn fast_forward_merge_yields_source_state() {
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let main = init_and_load(&dir).await;
let base_count = count_rows(&main, "node:Person").await;
main.branch_create("feature").await.unwrap();
let mut feature = Omnigraph::open(uri).await.unwrap();
append_new_persons(&mut feature, "feature", 5).await;
let source_count = count_rows_branch(&feature, "feature", "node:Person").await;
assert_eq!(source_count, base_count + 5);
let outcome = main.branch_merge("feature", "main").await.unwrap();
assert_eq!(outcome, MergeOutcome::FastForward);
assert_eq!(count_rows(&main, "node:Person").await, source_count);
let names = collect_column_strings(&read_table(&main, "node:Person").await, "name");
for i in 0..5 {
assert!(
names.contains(&format!("ff_new_{i}")),
"merged main missing new person ff_new_{i}; have {names:?}"
);
}
}
const VEC_SCHEMA: &str = "node Chunk {\n slug: String @key\n embedding: Vector(8) @index\n}\n";
#[tokio::test]
async fn fast_forward_merge_defers_vector_index_to_reconciler() {
use omnigraph::loader::LoadMode;
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let main = Omnigraph::init(uri, VEC_SCHEMA).await.unwrap();
main.branch_create("feature").await.unwrap();
let mut rows = String::new();
for i in 0..24 {
let v: Vec<String> = (0..8).map(|j| format!("{}.0", (i + j) % 5)).collect();
rows.push_str(&format!(
"{{\"type\":\"Chunk\",\"data\":{{\"slug\":\"c{i}\",\"embedding\":[{}]}}}}\n",
v.join(",")
));
}
let feature = Omnigraph::open(uri).await.unwrap();
feature.load("feature", &rows, LoadMode::Merge).await.unwrap();
let probes = MergeWriteProbes::default();
let outcome = with_merge_write_probes(probes.clone(), main.branch_merge("feature", "main"))
.await
.unwrap();
assert_eq!(outcome, MergeOutcome::FastForward);
assert_eq!(
probes.create_vector_index_calls(),
0,
"fast-forward adopt merge must defer vector-index coverage to the reconciler \
(0 inline IVF builds); did {}",
probes.create_vector_index_calls(),
);
assert_eq!(count_rows(&main, "node:Chunk").await, 24);
}
const BLOB_SCHEMA: &str = "node Document {\n title: String @key\n content: Blob?\n note: String?\n}\n";
const BLOB_INSERT: &str = r#"
query insert_doc($title: String, $content: Blob, $note: String) {
insert Document { title: $title, content: $content, note: $note }
}
"#;
#[tokio::test]
async fn fast_forward_merge_streams_blob_columns() {
use omnigraph::loader::{LoadMode, load_jsonl};
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
let mut main = Omnigraph::init(uri, BLOB_SCHEMA).await.unwrap();
load_jsonl(
&mut main,
"{\"type\":\"Document\",\"data\":{\"title\":\"seed\",\"content\":\"base64:U2VlZA==\",\"note\":\"base\"}}",
LoadMode::Overwrite,
)
.await
.unwrap();
main.branch_create("feature").await.unwrap();
let mut feature = Omnigraph::open(uri).await.unwrap();
mutate_branch(
&mut feature,
"feature",
BLOB_INSERT,
"insert_doc",
¶ms(&[
("$title", "readme"),
("$content", "base64:SGVsbG8="),
("$note", "branch"),
]),
)
.await
.unwrap();
let outcome = main.branch_merge("feature", "main").await.unwrap();
assert_eq!(outcome, MergeOutcome::FastForward);
let readme = main.read_blob("Document", "readme", "content").await.unwrap();
assert_eq!(&readme.read().await.unwrap()[..], b"Hello");
let seed = main.read_blob("Document", "seed", "content").await.unwrap();
assert_eq!(&seed.read().await.unwrap()[..], b"Seed");
}