Support multi-statement mutations (insert + edge in one query)

Allow mutation queries to contain multiple sequential statements that
execute atomically within a single transactional run. This enables
patterns like inserting a node and its edges in one query:

    query add_and_link($name: String, $age: I32, $friend: String) {
        insert Person { name: $name, age: $age }
        insert Knows { from: $name, to: $friend }
    }

Changes span the full compiler-to-execution pipeline:
- Grammar: mutation_body = { mutation_stmt+ }
- AST: QueryDecl.mutations: Vec<Mutation>
- IR: MutationIR.ops: Vec<MutationOpIR>
- Execution: loop over ops, accumulate affected counts

Cross-statement visibility works because each statement's commit_updates
advances the manifest state, so subsequent statements see prior writes.
Atomicity comes from the existing run mechanism (begin_run/publish_run).

https://claude.ai/code/session_01E4VG2WXrZW8aeXFiqr8NwF
This commit is contained in:
Claude 2026-04-11 20:27:51 +00:00
parent a844e0ba68
commit d10f78530f
No known key found for this signature in database
9 changed files with 240 additions and 64 deletions

View file

@ -3335,24 +3335,30 @@ impl Omnigraph {
let ir = lower_mutation_query(&query_decl)?;
match &ir.op {
MutationOpIR::Insert {
type_name,
assignments,
} => self.execute_insert(type_name, assignments, params).await,
MutationOpIR::Update {
type_name,
assignments,
predicate,
} => {
self.execute_update(type_name, assignments, predicate, params)
.await
}
MutationOpIR::Delete {
type_name,
predicate,
} => self.execute_delete(type_name, predicate, params).await,
let mut total = MutationResult::default();
for op in &ir.ops {
let result = match op {
MutationOpIR::Insert {
type_name,
assignments,
} => self.execute_insert(type_name, assignments, params).await?,
MutationOpIR::Update {
type_name,
assignments,
predicate,
} => {
self.execute_update(type_name, assignments, predicate, params)
.await?
}
MutationOpIR::Delete {
type_name,
predicate,
} => self.execute_delete(type_name, predicate, params).await?,
};
total.affected_nodes += result.affected_nodes;
total.affected_edges += result.affected_edges;
}
Ok(total)
}
pub async fn branch_merge(&mut self, source: &str, target: &str) -> Result<MergeOutcome> {

View file

@ -644,6 +644,43 @@ async fn mutation_insert_edge() {
assert_eq!(names.value(0), "Alice");
}
#[tokio::test]
async fn mutation_multi_insert_node_and_edge() {
let dir = tempfile::tempdir().unwrap();
let mut db = init_and_load(&dir).await;
// In one atomic mutation: insert Eve + edge Eve→Alice
let result = mutate_main(
&mut db,
MUTATION_QUERIES,
"insert_person_and_friend",
&mixed_params(&[("$name", "Eve"), ("$friend", "Alice")], &[("$age", 22)]),
)
.await
.unwrap();
assert_eq!(result.affected_nodes, 1);
assert_eq!(result.affected_edges, 1);
// Verify traversal: Eve → Alice
let qr = query_main(
&mut db,
TEST_QUERIES,
"friends_of",
&params(&[("$name", "Eve")]),
)
.await
.unwrap();
assert_eq!(qr.num_rows(), 1);
let batch = qr.concat_batches().unwrap();
let names = batch
.column(0)
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
assert_eq!(names.value(0), "Alice");
}
#[tokio::test]
async fn mutation_update_node() {
let dir = tempfile::tempdir().unwrap();

View file

@ -35,6 +35,11 @@ query remove_person($name: String) {
query remove_friendship($from: String) {
delete Knows where from = $from
}
query insert_person_and_friend($name: String, $age: I32, $friend: String) {
insert Person { name: $name, age: $age }
insert Knows { from: $name, to: $friend }
}
"#;
/// Init a repo and load the standard test data.