use lix::Value;
use serde_json::json;
use super::select_rows;
simulation_test!(
relation_diff_pairs_schema_columns_and_inverts_sides,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
let baseline = sim.initial_commit_id().to_string();
session
.execute(
"INSERT INTO lix_key_value (key, value) VALUES ('note', 'first')",
&[],
)
.await
.expect("tracked insert should succeed");
let inserted = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("inserted head should load")
.expect("inserted head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT key, diff_type, from_value, to_value \
FROM lix_diff('lix_key_value', '{baseline}', '{inserted}')"
),
)
.await,
vec![vec![
Value::Text("note".to_string()),
Value::Text("added".to_string()),
Value::Null,
Value::Jsonb(json!("first").into()),
]]
);
assert_eq!(
select_rows(
&session,
&format!(
"SELECT key, diff_type, from_value, to_value \
FROM lix_diff('lix_key_value', '{inserted}', '{baseline}')"
),
)
.await,
vec![vec![
Value::Text("note".to_string()),
Value::Text("removed".to_string()),
Value::Jsonb(json!("first").into()),
Value::Null,
]]
);
assert_eq!(
select_rows(
&session,
&format!(
"SELECT count(*) FROM lix_diff('lix_key_value', '{inserted}', '{inserted}')"
),
)
.await,
vec![vec![Value::Integer(0)]]
);
assert_eq!(
select_rows(
&session,
"SELECT count(*) FROM lix_diff(\
'lix_key_value', lix_root_commit_id(), lix_active_branch_commit_id()\
) WHERE key = 'note'",
)
.await,
vec![vec![Value::Integer(1)]],
"commit graph accessors are valid explicit table-function arguments",
);
session
.execute(
"UPDATE lix_key_value SET value = 'second' WHERE key = 'note'",
&[],
)
.await
.expect("tracked update should succeed");
let updated = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("updated head should load")
.expect("updated head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_value, to_value \
FROM lix_diff('lix_key_value', '{inserted}', '{updated}') \
WHERE key = 'note'"
),
)
.await,
vec![vec![
Value::Text("modified".to_string()),
Value::Jsonb(json!("first").into()),
Value::Jsonb(json!("second").into()),
]]
);
}
);
simulation_test!(
relation_diff_preserves_global_scope_on_both_sides,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
let local_before = sim.initial_commit_id().to_string();
let global_before = engine
.load_branch_head_commit_id(lix::GLOBAL_BRANCH_ID)
.await
.expect("global head should load")
.expect("global head should exist")
.to_string();
session
.execute(
"INSERT INTO lix_key_value (key, value) VALUES ('local-scope', 'value')",
&[],
)
.await
.expect("branch-local insert should succeed");
let local_after = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("local head should load")
.expect("local head should exist")
.to_string();
session
.execute("INSERT INTO lix_key_value (key, value, lixcol_global) VALUES ('global-scope', 'value', TRUE)", &[])
.await
.expect("global row should publish");
let global_after = engine
.load_branch_head_commit_id(lix::GLOBAL_BRANCH_ID)
.await
.expect("updated global head should load")
.expect("updated global head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT from_lixcol_global, to_lixcol_global \
FROM lix_diff('lix_key_value', '{global_before}', '{global_after}')"
),
)
.await,
vec![vec![Value::Null, Value::Boolean(true)]],
"global additions preserve the source relation's scope",
);
assert_eq!(
select_rows(
&session,
&format!(
"SELECT from_lixcol_global, to_lixcol_global \
FROM lix_diff('lix_key_value', '{global_after}', '{global_before}')"
),
)
.await,
vec![vec![Value::Boolean(true), Value::Null]],
"reversing a global diff preserves its global before-side",
);
assert_eq!(
select_rows(
&session,
&format!(
"SELECT from_lixcol_global, to_lixcol_global \
FROM lix_diff('lix_key_value', '{local_before}', '{local_after}')"
),
)
.await,
vec![vec![Value::Null, Value::Boolean(false)]],
"branch-local relation rows remain local in diff metadata",
);
}
);
simulation_test!(
working_diff_span_reports_global_provenance,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
session
.execute(
"INSERT INTO lix_key_value (key, value, lixcol_global) \
VALUES ('shadowed', 'global-value', TRUE)",
&[],
)
.await
.expect("global insert should succeed");
session
.create_checkpoint()
.await
.expect("checkpoint should pin the working-diff cursor");
session
.execute(
"INSERT INTO lix_key_value (key, value) VALUES ('shadowed', 'local-shadow') \
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
&[],
)
.await
.expect("active upsert should shadow the inherited global row");
assert_eq!(
select_rows(&session,
"SELECT diff_type, from_value, to_value FROM lix_diff('lix_key_value') WHERE key = 'shadowed'",
).await,
vec![vec![Value::Text("modified".into()), Value::Jsonb(json!("global-value").into()), Value::Jsonb(json!("local-shadow").into())]],
"effective classification and values do not depend on projecting provenance",
);
assert_eq!(
select_rows(
&session,
"SELECT from_lixcol_global, to_lixcol_global \
FROM lix_diff('lix_key_value') \
WHERE key = 'shadowed'",
)
.await,
vec![vec![Value::Boolean(true), Value::Boolean(false)]],
"the shadowed row's before-side is the inherited global version",
);
}
);
simulation_test!(
relation_diff_aggregates_files_and_preserves_removed_paths,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
let baseline = sim.initial_commit_id().to_string();
session
.execute(
"INSERT INTO lix_file (path, content) VALUES ('/note.txt', CAST('hello' AS BYTEA))",
&[],
)
.await
.expect("file insert should succeed");
let inserted = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("inserted head should load")
.expect("inserted head should exist")
.to_string();
let added = select_rows(
&session,
&format!(
"SELECT id, diff_type, from_path, to_path \
FROM lix_diff('lix_file', '{baseline}', '{inserted}')"
),
)
.await;
assert_eq!(added.len(), 1, "descriptor and content form one file diff");
assert_eq!(added[0][1], Value::Text("added".to_string()));
assert_eq!(added[0][2], Value::Null);
assert_eq!(added[0][3], Value::Text("/note.txt".to_string()));
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path \
FROM lix_diff('lix_file', '{inserted}', '{baseline}')"
),
)
.await,
vec![vec![
Value::Text("removed".to_string()),
Value::Text("/note.txt".to_string()),
Value::Null,
]],
"reversing a file addition swaps sides and inverts its classification",
);
let file_id = match &added[0][0] {
Value::Text(id) => id.clone(),
other => panic!("file row identity should be text, got {other:?}"),
};
assert_eq!(
select_rows(
&session,
&format!(
"SELECT count(*) FROM lix_diff('lix_file', '{baseline}', '{inserted}') \
WHERE id = '{file_id}'"
),
)
.await,
vec![vec![Value::Integer(1)]],
"typed primary-key equality remains a pushed file-scoped filter",
);
assert_eq!(
select_rows(
&session,
&format!(
"SELECT count(*) \
FROM lix_diff('lix_file', '{baseline}', '{inserted}')"
),
)
.await,
vec![vec![Value::Integer(1)]],
"counts operate on logical file rows",
);
session
.execute(
"UPDATE lix_file SET path = '/renamed.txt' WHERE path = '/note.txt'",
&[],
)
.await
.expect("file rename should succeed");
let renamed = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("renamed head should load")
.expect("renamed head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path FROM lix_diff('lix_file', '{inserted}', '{renamed}')"
),
)
.await,
vec![vec![
Value::Text("modified".to_string()),
Value::Text("/note.txt".to_string()),
Value::Text("/renamed.txt".to_string()),
]]
);
session
.execute("DELETE FROM lix_file WHERE path = '/renamed.txt'", &[])
.await
.expect("file removal should succeed");
let removed = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("removed head should load")
.expect("removed head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path FROM lix_diff('lix_file', '{renamed}', '{removed}')"
),
)
.await,
vec![vec![
Value::Text("removed".to_string()),
Value::Text("/renamed.txt".to_string()),
Value::Null,
]]
);
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path \
FROM lix_diff('lix_file', '{removed}', '{renamed}')"
),
)
.await,
vec![vec![
Value::Text("added".to_string()),
Value::Null,
Value::Text("/renamed.txt".to_string()),
]],
"reversing a file removal restores its historical path",
);
}
);
simulation_test!(
default_range_diff_uses_certified_hot_epoch_without_changing_its_schema,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
session
.execute(
"INSERT INTO lix_file (path, content) VALUES \
('/one.txt', CAST('one' AS BYTEA)), ('/two.txt', CAST('two' AS BYTEA))",
&[],
)
.await
.expect("file inserts should succeed");
let rows = select_rows(
&session,
"SELECT coalesce(to_path, from_path) AS path, diff_type \
FROM lix_diff('lix_file') ORDER BY coalesce(to_path, from_path)",
)
.await;
assert_eq!(rows.len(), 2);
for (index, path) in ["/one.txt", "/two.txt"].into_iter().enumerate() {
assert_eq!(rows[index][0], Value::Text(path.to_string()));
assert_eq!(rows[index][1], Value::Text("added".to_string()));
}
assert_eq!(
select_rows(
&session,
"SELECT from_content, to_content FROM lix_diff('lix_file') ORDER BY to_path"
)
.await,
vec![
vec![Value::Null, Value::Blob(b"one".to_vec().into())],
vec![Value::Null, Value::Blob(b"two".to_vec().into())]
]
);
}
);
simulation_test!(
working_diff_remains_available_after_interval_local_add_then_delete,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
session
.execute(
"INSERT INTO lix_file (id, path, content) VALUES \
('87851c4d-9308-46af-9046-5aa5351250fd', '/net-zero.md', CAST('temporary' AS BYTEA))",
&[],
)
.await
.expect("file insert should succeed");
session
.execute(
"DELETE FROM lix_file WHERE id = '87851c4d-9308-46af-9046-5aa5351250fd'",
&[],
)
.await
.expect("file delete should succeed");
assert!(
select_rows(&session, "SELECT id, diff_type FROM lix_diff('lix_file')",)
.await
.is_empty(),
"net-zero file churn must not appear in working review",
);
}
);
simulation_test!(
relation_diff_tracks_directory_descriptor_add_rename_and_remove,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
let baseline = sim.initial_commit_id().to_string();
session
.execute("INSERT INTO lix_directory (path) VALUES ('/docs')", &[])
.await
.expect("directory insert should succeed");
let inserted = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("inserted head should load")
.expect("inserted head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path \
FROM lix_diff('lix_directory', '{baseline}', '{inserted}')"
),
)
.await,
vec![vec![
Value::Text("added".to_string()),
Value::Null,
Value::Text("/docs".to_string()),
]]
);
session
.execute(
"UPDATE lix_directory SET path = '/archive' WHERE path = '/docs'",
&[],
)
.await
.expect("directory rename should succeed");
let renamed = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("renamed head should load")
.expect("renamed head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path \
FROM lix_diff('lix_directory', '{inserted}', '{renamed}')"
),
)
.await,
vec![vec![
Value::Text("modified".to_string()),
Value::Text("/docs".to_string()),
Value::Text("/archive".to_string()),
]]
);
session
.execute("DELETE FROM lix_directory WHERE path = '/archive'", &[])
.await
.expect("directory deletion should succeed");
let removed = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("removed head should load")
.expect("removed head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_path, to_path \
FROM lix_diff('lix_directory', '{renamed}', '{removed}')"
),
)
.await,
vec![vec![
Value::Text("removed".to_string()),
Value::Text("/archive".to_string()),
Value::Null,
]]
);
}
);
simulation_test!(
relation_diff_accepts_default_range_and_rejects_partial_range,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
let missing_relation = session
.execute("SELECT * FROM lix_diff('a', 'b')", &[])
.await
.expect_err("legacy two-argument lix_diff must be rejected");
assert!(
missing_relation
.message
.contains("requires a relation and either zero or two commit ID arguments")
);
let unsupported = session
.execute("SELECT * FROM lix_diff('not_a_relation', 'a', 'b')", &[])
.await
.expect_err("unknown relations must be rejected");
assert!(unsupported.message.contains("does not support relation"));
let file_content = session.execute(
"SELECT from_content, to_content FROM lix_diff('lix_file', lix_root_commit_id(), lix_active_branch_commit_id())", &[])
.await.expect("empty file diffs support content projection");
assert_eq!(file_content.columns(), ["from_content", "to_content"]);
}
);
simulation_test!(
relation_diff_fences_machinery_from_user_column_names,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
let reserved = session
.execute(
"INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
VALUES (CAST('{\"$schema\":\"https://lix.dev/schema-v1.json\",\"key\":\"reserved_name_collision\",\"columns\":[{\"name\":\"id\",\"type\":\"text\",\"nullable\":false},{\"name\":\"from_lixcol_user_value\",\"type\":\"text\",\"nullable\":true}],\"primary_key\":[\"id\"]}' AS JSONB), false, false)",
&[],
)
.await
.expect_err("schema registration must reject the reserved lixcol_ segment");
assert!(reserved.message.contains("reserved lixcol_ segment"));
session
.execute(
"INSERT INTO lix_registered_schema (value, lixcol_global, lixcol_untracked) \
VALUES (CAST('{\"$schema\":\"https://lix.dev/schema-v1.json\",\"key\":\"diff_name_collision\",\"columns\":[{\"name\":\"id\",\"type\":\"text\",\"nullable\":false},{\"name\":\"diff_type\",\"type\":\"text\",\"nullable\":false},{\"name\":\"row_count\",\"type\":\"int8\",\"nullable\":false},{\"name\":\"depth\",\"type\":\"int8\",\"nullable\":false},{\"name\":\"from_path\",\"type\":\"text\",\"nullable\":false}],\"primary_key\":[\"id\"]}' AS JSONB), false, false)",
&[],
)
.await
.expect("collision schema should register");
let baseline = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("baseline head should load")
.expect("baseline head should exist")
.to_string();
session
.execute(
"INSERT INTO diff_name_collision (id, diff_type, row_count, depth, from_path) \
VALUES ('row-1', 'user-kind', 7, 3, '/user-path')",
&[],
)
.await
.expect("collision row should insert");
let head = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.expect("updated head should load")
.expect("updated head should exist")
.to_string();
assert_eq!(
select_rows(
&session,
&format!(
"SELECT id, diff_type, \
from_diff_type, to_diff_type, from_row_count, to_row_count, \
from_depth, to_depth, from_from_path, to_from_path \
FROM lix_diff('diff_name_collision', '{baseline}', '{head}')"
),
)
.await,
vec![vec![
Value::Text("row-1".to_string()),
Value::Text("added".to_string()),
Value::Null,
Value::Text("user-kind".to_string()),
Value::Null,
Value::Integer(7),
Value::Null,
Value::Integer(3),
Value::Null,
Value::Text("/user-path".to_string()),
]]
);
for retired in ["lixcol_diff_type", "lixcol_row_count", "row_count"] {
let error = session
.execute(
&format!(
"SELECT {retired} FROM lix_diff('diff_name_collision', '{baseline}', '{head}')"
),
&[],
)
.await
.expect_err("retired diff machinery alias must not exist");
assert!(
error.message.contains(retired),
"error should identify retired column {retired}: {}",
error.message
);
assert!(
retired == "row_count" || error.message.contains("Did you mean"),
"renamed diff columns should preserve DataFusion's near-match guidance: {}",
error.message
);
}
let error = session
.execute(
&format!(
"SELECT not_a_diff_column FROM lix_diff('diff_name_collision', '{baseline}', '{head}')"
),
&[],
)
.await
.expect_err("unknown diff column must not exist");
assert!(
error.message.contains("Valid fields are")
&& error.message.contains("diff_type")
&& error.message.contains("row_count"),
"unknown diff columns should preserve DataFusion's discoverable field listing: {}",
error.message
);
}
);
simulation_test!(
working_diff_retains_relation_payloads_on_both_sides,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
session
.execute(
"INSERT INTO lix_key_value (key, value, lixcol_metadata) VALUES \
('modified', 'before', '{\"stage\":\"before\"}'), ('removed', 'deleted', NULL)",
&[],
)
.await
.expect("baseline rows should insert");
session
.create_checkpoint()
.await
.expect("checkpoint should succeed");
for sql in [
"INSERT INTO lix_key_value (key, value) VALUES ('added', 'new')",
"UPDATE lix_key_value SET value = 'after', lixcol_metadata = '{\"stage\":\"after\"}' WHERE key = 'modified'",
"DELETE FROM lix_key_value WHERE key = 'removed'",
] {
session
.execute(sql, &[])
.await
.expect("working edit should succeed");
}
assert_eq!(
select_rows(
&session,
"SELECT key, diff_type FROM lix_diff('lix_key_value') ORDER BY key"
)
.await,
vec![
vec![Value::Text("added".into()), Value::Text("added".into())],
vec![
Value::Text("modified".into()),
Value::Text("modified".into())
],
vec![Value::Text("removed".into()), Value::Text("removed".into())],
],
"identity-only projections must preserve the same changed rows",
);
let projection =
"key, diff_type, from_value, to_value, from_lixcol_global, to_lixcol_global";
let expected = vec![
vec![
Value::Text("added".into()),
Value::Text("added".into()),
Value::Null,
Value::Jsonb(json!("new").into()),
Value::Null,
Value::Boolean(false),
],
vec![
Value::Text("modified".into()),
Value::Text("modified".into()),
Value::Jsonb(json!("before").into()),
Value::Jsonb(json!("after").into()),
Value::Boolean(false),
Value::Boolean(false),
],
vec![
Value::Text("removed".into()),
Value::Text("removed".into()),
Value::Jsonb(json!("deleted").into()),
Value::Null,
Value::Boolean(false),
Value::Null,
],
];
let coordinates = session.execute("SELECT working_base_commit_id, commit_id FROM lix_branch WHERE id = lix_active_branch_id()", &[]).await.unwrap();
let from = coordinates.rows()[0]
.get::<String>("working_base_commit_id")
.unwrap();
let to = coordinates.rows()[0].get::<String>("commit_id").unwrap();
for arguments in [
"'lix_key_value'".to_string(),
format!("'lix_key_value', '{from}', '{to}'"),
] {
assert_eq!(
select_rows(
&session,
&format!("SELECT {projection} FROM lix_diff({arguments}) ORDER BY key")
)
.await,
expected,
"working and explicit ranges must preserve payload values and side presence: {arguments}",
);
}
assert_eq!(
select_rows(
&session,
"SELECT from_lixcol_metadata, to_lixcol_metadata \
FROM lix_diff('lix_key_value') WHERE key = 'modified'"
)
.await,
vec![vec![
Value::Jsonb(json!({"stage": "before"}).into()),
Value::Jsonb(json!({"stage": "after"}).into()),
]],
"metadata must be loaded from each side's exact change",
);
assert_eq!(
select_rows(&session,
"SELECT key FROM lix_diff('lix_key_value') WHERE from_value IS NOT NULL AND to_value IS NOT NULL"
).await,
vec![vec![Value::Text("modified".into())]],
"payloads used only by a predicate must also be retained",
);
}
);
simulation_test!(
working_diff_retains_directory_descriptors_on_both_sides,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(
engine.open_session().await.expect("session should open"),
&engine,
);
session
.execute(
"INSERT INTO lix_directory (path) VALUES ('/docs/original'), ('/docs/removed')",
&[],
)
.await
.expect("nested baseline directories should insert");
session
.create_checkpoint()
.await
.expect("checkpoint should succeed");
for sql in [
"INSERT INTO lix_directory (path) VALUES ('/docs/added')",
"UPDATE lix_directory SET path = '/docs/renamed' WHERE path = '/docs/original'",
"DELETE FROM lix_directory WHERE path = '/docs/removed'",
] {
session
.execute(sql, &[])
.await
.expect("working edit should succeed");
}
let expected = vec![
vec![
Value::Text("added".into()),
Value::Null,
Value::Text("added".into()),
Value::Null,
Value::Text("/docs/added".into()),
],
vec![
Value::Text("removed".into()),
Value::Text("removed".into()),
Value::Null,
Value::Text("/docs/removed".into()),
Value::Null,
],
vec![
Value::Text("modified".into()),
Value::Text("original".into()),
Value::Text("renamed".into()),
Value::Text("/docs/original".into()),
Value::Text("/docs/renamed".into()),
],
];
let coordinates = session.execute("SELECT working_base_commit_id, commit_id FROM lix_branch WHERE id = lix_active_branch_id()", &[]).await.unwrap();
let from = coordinates.rows()[0]
.get::<String>("working_base_commit_id")
.unwrap();
let to = coordinates.rows()[0].get::<String>("commit_id").unwrap();
for arguments in [
"'lix_directory'".to_string(),
format!("'lix_directory', '{from}', '{to}'"),
] {
assert_eq!(
select_rows(
&session,
&format!(
"SELECT diff_type, from_name, to_name, from_path, to_path \
FROM lix_diff({arguments}) ORDER BY coalesce(to_path, from_path)"
)
)
.await,
expected,
"working and explicit ranges must reconstruct paths through unchanged parents: {arguments}",
);
}
}
);
simulation_test!(
file_diff_content_preserves_absent_empty_and_renamed_sides,
|sim| async move {
let engine = sim.boot_engine().await;
let session = sim.wrap_session(engine.open_session().await.unwrap(), &engine);
let id = "0193182b-2a72-7ed5-9015-76bf271af333";
let steps = [
(
"INSERT INTO lix_file(id,path,content) VALUES ($1, '/empty', CAST('' AS BYTEA))",
"added",
Value::Null,
Value::Blob(Vec::new().into()),
),
(
"UPDATE lix_file SET path='/renamed' WHERE id=$1",
"modified",
Value::Blob(Vec::new().into()),
Value::Blob(Vec::new().into()),
),
(
"UPDATE lix_file SET content=CAST('next' AS BYTEA) WHERE id=$1",
"modified",
Value::Blob(Vec::new().into()),
Value::Blob(b"next".to_vec().into()),
),
(
"DELETE FROM lix_file WHERE id=$1",
"removed",
Value::Blob(b"next".to_vec().into()),
Value::Null,
),
];
for (sql, kind, old, new) in steps {
let before = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.unwrap()
.unwrap()
.to_string();
session
.execute(sql, &[Value::Text(id.into())])
.await
.unwrap();
let after = engine
.load_branch_head_commit_id(sim.main_branch_id())
.await
.unwrap()
.unwrap()
.to_string();
for (commit, content) in [(&before, &old), (&after, &new)] {
let expected = if *content == Value::Null {
Vec::new()
} else {
vec![vec![content.clone()]]
};
assert_eq!(
select_rows(
&session,
&format!(
"SELECT content FROM lix_as_of('lix_file', '{commit}') WHERE id='{id}'"
),
)
.await,
expected,
"snapshot and diff content must agree at {commit}",
);
}
for source in [
format!("lix_diff('lix_file', '{before}', '{after}')"),
format!("lix_history('lix_file', '{after}')"),
] {
assert_eq!(select_rows(&session, &format!("SELECT diff_type, from_content, to_content FROM {source} WHERE id='{id}' AND lixcol_to_commit_id='{after}'")).await,
vec![vec![Value::Text(kind.into()), old.clone(), new.clone()]]);
}
assert_eq!(select_rows(&session, &format!("SELECT from_content, to_content FROM lix_diff('lix_file', '{after}', '{before}') WHERE id='{id}'")).await,
vec![vec![new, old]]);
}
}
);