use crate::model::{Node, NodeLabel};
use crate::storage::capability::Storage;
use crate::storage::repository::Repository;
#[test]
fn in_write_transaction_survives_delete_and_copy() {
let dir = tempfile::TempDir::new().unwrap();
let db = dir.path().join("tx_probe");
let repo = Repository::open(&db).unwrap();
repo.init_schema().unwrap();
let node = Node::builder(NodeLabel::File, "probe/a.rs", "probe/a.rs")
.id("file_probe1")
.project("p_probe")
.build();
repo.save_nodes_stream(std::iter::once(&node), NodeLabel::File)
.unwrap();
let result: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
tx.execute("MATCH (n:File) WHERE n.id = 'file_probe1' DELETE n;")?;
let replacement = Node::builder(NodeLabel::File, "probe/b.rs", "probe/b.rs")
.id("file_probe2")
.project("p_probe")
.build();
Repository::save_nodes_stream_on(tx, std::iter::once(&replacement), NodeLabel::File)
});
result.expect("committed transaction");
let rows = repo.query("MATCH (n:File) RETURN n.id AS id;").unwrap();
let ids: Vec<String> = rows
.iter()
.filter_map(|r| r.first().and_then(|v| v.as_str()).map(String::from))
.collect();
assert!(
!ids.contains(&"file_probe1".to_string()),
"DELETE inside tx must be visible after commit, got {ids:?}"
);
assert!(
ids.contains(&"file_probe2".to_string()),
"COPY inside tx must be visible after commit, got {ids:?}"
);
}
#[test]
fn in_write_transaction_rolls_back_on_err() {
let dir = tempfile::TempDir::new().unwrap();
let db = dir.path().join("tx_probe_rollback");
let repo = Repository::open(&db).unwrap();
repo.init_schema().unwrap();
let node = Node::builder(NodeLabel::File, "probe/c.rs", "probe/c.rs")
.id("file_probe3")
.project("p_probe")
.build();
let result: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
Repository::save_nodes_stream_on(tx, std::iter::once(&node), NodeLabel::File)?;
Err(crate::storage::error::StorageError::InvalidData(
"deliberate failure".to_string(),
))
});
assert!(result.is_err(), "closure error must propagate");
let rows = repo
.query("MATCH (n:File) WHERE n.id = 'file_probe3' RETURN count(n.id) AS cnt;")
.unwrap();
let cnt = rows
.first()
.and_then(|r| r.first())
.and_then(|v| v.as_i64())
.unwrap_or(0);
assert_eq!(cnt, 0, "staged COPY must be rolled back");
}
#[test]
fn probe_batch_delete_in_tx() {
let dir = tempfile::TempDir::new().unwrap();
let db = dir.path().join("tx_probe_batch");
let repo = Repository::open(&db).unwrap();
repo.init_schema().unwrap();
let node = Node::builder(NodeLabel::File, "probe/d.rs", "probe/d.rs")
.id("file_probe4")
.project("p_probe")
.build();
repo.save_nodes_stream(std::iter::once(&node), NodeLabel::File)
.unwrap();
let result: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
Repository::delete_file_nodes_batch_on(tx, &["probe/d.rs".to_string()], "p_probe")
});
println!("[probe-batch] result: {:?}", result.as_ref().map(|_| "ok"));
assert!(
result.is_ok(),
"batch delete in tx failed: {:?}",
result.err()
);
use crate::storage::capability::Storage;
let rows = repo
.query("MATCH (n:Function) WHERE n.filePath = 'probe/d.rs' RETURN count(n.id) AS cnt;")
.unwrap();
let cnt = rows
.first()
.and_then(|r| r.first())
.and_then(|v| v.as_i64())
.unwrap_or(-1);
println!("[probe-batch] function rows after committed delete-tx: {cnt}");
assert_eq!(cnt, 0, "committed delete-tx must remove the rows");
}
#[test]
fn probe_which_statement_kills_tx() {
let dir = tempfile::TempDir::new().unwrap();
let db = dir.path().join("tx_probe_stmt");
let repo = Repository::open(&db).unwrap();
repo.init_schema().unwrap();
let r: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
tx.query("MATCH (n:File) WHERE n.filePath IN ['nope'] RETURN n.id AS id;")?;
Ok(())
});
println!("[stmt] SELECT in tx: {:?}", r.is_ok());
let r: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
tx.execute("MATCH (n:File) WHERE n.filePath IN ['nope'] AND n.project = 'p' DELETE n;")?;
Ok(())
});
println!("[stmt] DELETE in tx: {:?}", r.is_ok());
let r: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
tx.execute("MATCH (n:File) WHERE n.filePath IN ['nope'] AND n.project = 'p' DELETE n;")?;
tx.execute(
"MATCH (r:CodeRelation) WHERE r.source IN ['x'] OR r.target IN ['x'] DELETE r;",
)?;
Ok(())
});
println!("[stmt] DELETE+DELETE in tx: {:?}", r.is_ok());
let r: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
tx.query("MATCH (n:Process) WHERE n.filePath IN ['nope'] RETURN n.id AS id;")?;
Ok(())
});
println!("[stmt] SELECT-on-no-filePath-label in tx: {:?}", r.is_ok());
}
#[test]
fn probe_show_tables_columns() {
let dir = tempfile::TempDir::new().unwrap();
let db = dir.path().join("tx_probe_tables");
let repo = Repository::open(&db).unwrap();
repo.init_schema().unwrap();
let rows = repo.query("CALL show_tables() RETURN *;").unwrap();
println!("[tables] row count: {}", rows.len());
for r in rows.iter().take(3) {
println!("[tables] row: {r:?}");
}
}
#[test]
fn lbug_allows_delete_then_insert_same_pk_in_one_tx() {
let dir = tempfile::TempDir::new().unwrap();
let db = dir.path().join("tx_probe_pk_limit");
let repo = Repository::open(&db).unwrap();
repo.init_schema().unwrap();
let result: Result<(), crate::storage::error::StorageError> = repo.in_write_transaction(|tx| {
tx.execute(
"CREATE (:File {id: 'pk_probe', project: 'p', name: 'a.rs', \
filePath: 'a.rs', language: 'rust', hash: 'h', lineCount: 1});",
)?;
tx.execute("MATCH (n:File) WHERE n.id = 'pk_probe' DELETE n;")?;
tx.execute(
"CREATE (:File {id: 'pk_probe', project: 'p', name: 'a.rs', \
filePath: 'a.rs', language: 'rust', hash: 'h2', lineCount: 2});",
)
});
result.expect(
"delete+re-insert of the same PK in one tx must succeed; \
if this fails, LoadPhase full atomicity is broken",
);
}