grammar+db: 3g — RETURNING on INSERT/UPDATE/DELETE (ADR-0033 §5)
Shared RETURNING_CLAUSE (reuses Phase-2 PROJECTION_LIST, now pub(crate)) as an optional tail on all three SQL DML shapes. `returning: bool` on the Command variants, set by the ast-builders and threaded to the worker. run_returning collects the returned rows as a DataResult (RETURNING mutates + yields in one pass), reusing resolve_select_column_types for bare-column type recovery; computed projections stay typeless. DeleteResult gains a `data` field rendered alongside the cascade summary. Follow-set fix: `returning` is added to the table-source and projection bare-alias follow-sets so an INSERT … SELECT row source stops before RETURNING instead of reading it as a table alias. Auto-fill × RETURNING: build_sql_insert stops row_source before the RETURNING token (keeping it preparable for shortid materialisation), and plan_shortid_autofill re-appends the RETURNING tail so generated shortids surface in RETURNING *. Tests (+17): grammar accept on all three; INSERT/UPDATE/DELETE RETURNING incl. *, aliases, multi-row, type recovery + computed- typeless; auto-fill × RETURNING (single + multi-row distinct ids); INSERT…SELECT…RETURNING execution; UPDATE…RETURNING zero-match; DELETE…RETURNING cascade+rows; app-level render of both. Dev sql_insert/sql_update/sql_delete entry words still removed in 3j. 1562 pass / 0 fail / 1 ignored. Clippy clean.
This commit is contained in:
+43
-3
@@ -71,6 +71,7 @@ fn seed(db: &Database, rt: &tokio::runtime::Runtime, sql: &str, target: &str) {
|
||||
target.to_string(),
|
||||
Vec::new(),
|
||||
String::new(),
|
||||
false,
|
||||
))
|
||||
.unwrap_or_else(|e| panic!("seed {sql:?}: {e:?}"));
|
||||
}
|
||||
@@ -82,8 +83,8 @@ fn run_delete(
|
||||
input: &str,
|
||||
) -> Result<DeleteResult, DbError> {
|
||||
match parse_command(input).expect("parse sql_delete") {
|
||||
Command::SqlDelete { sql, target_table } => {
|
||||
rt.block_on(db.run_sql_delete(sql, Some(input.to_string()), target_table))
|
||||
Command::SqlDelete { sql, target_table, returning } => {
|
||||
rt.block_on(db.run_sql_delete(sql, Some(input.to_string()), target_table, returning))
|
||||
}
|
||||
other => panic!("expected Command::SqlDelete, got {other:?}"),
|
||||
}
|
||||
@@ -117,7 +118,7 @@ fn parse_path_lowers_sql_delete_to_command() {
|
||||
let command = parse_command("sql_delete from Orders where id = 1")
|
||||
.expect("sql_delete parses in advanced mode");
|
||||
match command {
|
||||
Command::SqlDelete { sql, target_table } => {
|
||||
Command::SqlDelete { sql, target_table, .. } => {
|
||||
assert_eq!(sql, "delete from Orders where id = 1");
|
||||
assert_eq!(target_table, "Orders");
|
||||
}
|
||||
@@ -424,3 +425,42 @@ fn internal_target_table_rejected_at_parse() {
|
||||
"internal table must be rejected at the DELETE target slot"
|
||||
);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Sub-phase 3g — RETURNING (ADR-0033 §5)
|
||||
// =================================================================
|
||||
|
||||
#[test]
|
||||
fn delete_returning_yields_predelete_row() {
|
||||
let (project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
create_cols(&db, &rt, "t", &[("id", Type::Int), ("v", Type::Text)], &["id"]);
|
||||
seed(&db, &rt, "insert into t (id, v) values (1, 'gone'), (2, 'keep')", "t");
|
||||
let result = run_delete(&db, &rt, "sql_delete from t where id = 1 returning *")
|
||||
.expect("DELETE … RETURNING * runs");
|
||||
assert_eq!(result.rows_affected, 1, "one row deleted");
|
||||
// RETURNING yields the row as it was BEFORE deletion.
|
||||
assert_eq!(result.data.rows.len(), 1, "the deleted row was returned");
|
||||
assert_eq!(result.data.rows[0][1], Some("gone".to_string()));
|
||||
// And it really is gone from the table.
|
||||
let csv = read_csv(&project, "t").expect("t.csv");
|
||||
assert!(!csv.contains("gone") && csv.contains("keep"), "row actually deleted: {csv:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_returning_with_cascade_surfaces_both() {
|
||||
// 3g: a parent DELETE … RETURNING must surface BOTH the returned
|
||||
// (deleted) parent rows AND the per-relationship cascade summary.
|
||||
let (_project, db, _dir) = open_project_db();
|
||||
let rt = rt();
|
||||
cascade_fixture(&db, &rt);
|
||||
let result = run_delete(&db, &rt, "sql_delete from Customers where id = 1 returning *")
|
||||
.expect("cascading DELETE … RETURNING runs");
|
||||
assert_eq!(result.rows_affected, 1, "one parent row deleted");
|
||||
// RETURNING gave the deleted customer row.
|
||||
assert_eq!(result.data.rows.len(), 1, "deleted parent row returned");
|
||||
// Cascade summary still computed alongside the result set.
|
||||
assert_eq!(result.cascade.len(), 1, "cascade reported");
|
||||
assert_eq!(result.cascade[0].child_table, "Orders");
|
||||
assert_eq!(result.cascade[0].rows_changed, 2, "both Alice's orders cascaded");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user