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:
claude@clouddev1
2026-05-22 20:44:55 +00:00
parent b935090d7b
commit fd8b74ba5e
12 changed files with 637 additions and 46 deletions
+13 -1
View File
@@ -14,7 +14,7 @@
//! sub-phases.
use crate::dsl::grammar::sql_expr;
use crate::dsl::grammar::sql_select::{SQL_SELECT_COMPOUND, reject_internal_table};
use crate::dsl::grammar::sql_select::{RETURNING_CLAUSE, SQL_SELECT_COMPOUND, reject_internal_table};
use crate::dsl::grammar::{IdentSource, Node, Word};
static COMMA: Node = Node::Punct(',');
@@ -110,6 +110,7 @@ static SQL_INSERT_TAIL_NODES: &[Node] = &[
TARGET_TABLE,
OPTIONAL_COLUMN_LIST,
ROW_SOURCE,
Node::Optional(&RETURNING_CLAUSE),
Node::Optional(&Node::Punct(';')),
];
@@ -184,6 +185,17 @@ mod tests {
good("into t values (case when 1 > 0 then 'y' else 'n' end)");
}
#[test]
fn returning_tail_admitted() {
// 3g: optional RETURNING projection_list tail, on both row
// sources.
good("into orders values (1, 2.0) returning *");
good("into orders (id, total) values (1, 2.0) returning id");
good("into orders values (1, 'a'), (2, 'b') returning id, total");
good("into archive select * from orders returning *");
good("into orders values (1) returning id as new_id;");
}
#[test]
fn internal_target_table_rejected() {
bad("into __rdbms_playground_columns values (1)");