#![cfg(feature = "std")]
use graphitesql::{ConflictAction as A, ConflictType as T, Connection, Rebaser};
use std::process::Command;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn changeset(base: &str, dml: &str) -> Vec<u8> {
let mut c = Connection::open_memory().unwrap();
c.execute_batch(base).unwrap();
let s = c.create_session();
s.attach();
c.execute_batch(dml).unwrap();
c.session_changeset(&s).unwrap()
}
fn policy(name: &str) -> impl FnMut(T) -> A + '_ {
move |k| match name {
"replace" => match k {
T::Data | T::Conflict => A::Replace,
_ => A::Omit,
},
_ => A::Omit,
}
}
fn graphite_rebase(base: &str, remote_dml: &str, local_dml: &str, pol: &str) -> String {
let remote = changeset(base, remote_dml);
let local = changeset(base, local_dml);
let mut b = Connection::open_memory().unwrap();
b.execute_batch(base).unwrap();
b.execute_batch(local_dml).unwrap();
let blob = b.changeset_apply_rebase(&remote, policy(pol)).unwrap();
let mut reb = Rebaser::new();
reb.configure(&blob).unwrap();
hex(&reb.rebase(&local).unwrap())
}
fn oracle_rebase(base: &str, remote_dml: &str, local_dml: &str, pol: &str) -> Option<String> {
let bin = std::env::var("GRAPHITE_SESREBASE").ok()?;
let remote = hex(&changeset(base, remote_dml));
let local = hex(&changeset(base, local_dml));
let bdb = format!("{base} {local_dml}");
let out = Command::new(bin)
.arg(":memory:")
.arg(bdb)
.arg(remote)
.arg(local)
.arg(pol)
.output()
.expect("run sesrebase");
assert!(
out.status.success(),
"sesrebase failed: {}",
String::from_utf8_lossy(&out.stderr)
);
Some(String::from_utf8_lossy(&out.stdout).trim().to_string())
}
fn check(base: &str, remote_dml: &str, local_dml: &str, pol: &str) {
let got = graphite_rebase(base, remote_dml, local_dml, pol);
if let Some(reference) = oracle_rebase(base, remote_dml, local_dml, pol) {
assert_eq!(
got, reference,
"rebase vs oracle\n base={base}\n remote={remote_dml}\n local={local_dml}\n policy={pol}"
);
}
}
const S: &str = "CREATE TABLE t(a INTEGER PRIMARY KEY, b);";
#[test]
fn update_vs_update_omit() {
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
"UPDATE t SET b='A' WHERE a=1;",
"UPDATE t SET b='B' WHERE a=1;",
"omit",
);
}
#[test]
fn update_vs_update_replace() {
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
"UPDATE t SET b='A' WHERE a=1;",
"UPDATE t SET b='B' WHERE a=1;",
"replace",
);
}
#[test]
fn insert_vs_insert_omit() {
check(
S,
"INSERT INTO t VALUES(1,'A');",
"INSERT INTO t VALUES(1,'B');",
"omit",
);
}
#[test]
fn insert_vs_insert_replace() {
check(
S,
"INSERT INTO t VALUES(1,'A');",
"INSERT INTO t VALUES(1,'B');",
"replace",
);
}
#[test]
fn update_vs_delete_omit() {
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
"DELETE FROM t WHERE a=1;",
"UPDATE t SET b='B' WHERE a=1;",
"omit",
);
}
#[test]
fn delete_vs_update_omit() {
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
"UPDATE t SET b='A' WHERE a=1;",
"DELETE FROM t WHERE a=1;",
"omit",
);
}
#[test]
fn delete_vs_delete_omit() {
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
"DELETE FROM t WHERE a=1;",
"DELETE FROM t WHERE a=1;",
"omit",
);
}
#[test]
fn no_conflict_passthrough() {
check(
&format!("{S} INSERT INTO t VALUES(1,'x'),(2,'y');"),
"UPDATE t SET b='A' WHERE a=1;",
"UPDATE t SET b='Y' WHERE a=2;",
"omit",
);
}
#[test]
fn multicol_partial_update_omit() {
let s = "CREATE TABLE t(a INTEGER PRIMARY KEY, b, c);";
check(
&format!("{s} INSERT INTO t VALUES(1,'b0','c0');"),
"UPDATE t SET b='bA' WHERE a=1;",
"UPDATE t SET c='cB' WHERE a=1;",
"omit",
);
}
#[test]
fn multicol_partial_update_replace() {
let s = "CREATE TABLE t(a INTEGER PRIMARY KEY, b, c);";
check(
&format!("{s} INSERT INTO t VALUES(1,'b0','c0');"),
"UPDATE t SET b='bA' WHERE a=1;",
"UPDATE t SET b='bB', c='cB' WHERE a=1;",
"replace",
);
}
#[test]
fn text_pk_update_vs_update_omit() {
let s = "CREATE TABLE t(a TEXT PRIMARY KEY, b);";
check(
&format!("{s} INSERT INTO t VALUES('k','base');"),
"UPDATE t SET b='A' WHERE a='k';",
"UPDATE t SET b='B' WHERE a='k';",
"omit",
);
}
#[test]
fn multi_row_mixed() {
let base = format!("{S} INSERT INTO t VALUES(1,'x'),(2,'y'),(3,'z');");
check(
&base,
"UPDATE t SET b='A2' WHERE a=2; DELETE FROM t WHERE a=3;",
"UPDATE t SET b='B1' WHERE a=1; UPDATE t SET b='B2' WHERE a=2; UPDATE t SET b='B3' WHERE a=3;",
"omit",
);
check(
&base,
"UPDATE t SET b='A2' WHERE a=2;",
"UPDATE t SET b='B2' WHERE a=2;",
"replace",
);
}
#[test]
fn value_types_update_conflict() {
let vals = ["42", "-7.5", "'text'", "x'00ff10'", "NULL"];
for v in vals {
for pol in ["omit", "replace"] {
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
&format!("UPDATE t SET b={v} WHERE a=1;"),
"UPDATE t SET b='local' WHERE a=1;",
pol,
);
}
}
}
#[test]
fn fuzz_update_and_insert_conflicts() {
let vs = ["10", "20", "'p'", "'q'", "3.5", "x'ab'"];
let mut n = 0;
for (i, rv) in vs.iter().enumerate() {
for (j, lv) in vs.iter().enumerate() {
let pol = if (i + j) % 2 == 0 { "omit" } else { "replace" };
check(
&format!("{S} INSERT INTO t VALUES(1,'base');"),
&format!("UPDATE t SET b={rv} WHERE a=1;"),
&format!("UPDATE t SET b={lv} WHERE a=1;"),
pol,
);
check(
S,
&format!("INSERT INTO t VALUES(1,{rv});"),
&format!("INSERT INTO t VALUES(1,{lv});"),
pol,
);
n += 2;
}
}
assert!(n >= 72);
}