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:
+37
-7
@@ -890,10 +890,19 @@ fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
// The row source is everything from the `VALUES` / `SELECT` /
|
||||
// `WITH` keyword onward. Located by the first matching *Word
|
||||
// token* in the path (not a text scan), so a string literal
|
||||
// like `values ('select')` can't be mistaken for the keyword.
|
||||
// The row source is the `VALUES` / `SELECT` / `WITH` clause —
|
||||
// from that keyword up to (but not including) any `RETURNING`
|
||||
// tail (3g) or trailing `;`. Both boundaries are located by
|
||||
// *Word token* in the path (not a text scan), so a string
|
||||
// literal like `values ('select')` / `values ('returning')`
|
||||
// can't be mistaken for a keyword. Excluding RETURNING keeps the
|
||||
// row source independently preparable for `shortid` auto-fill
|
||||
// (`VALUES … RETURNING …` is not a valid standalone statement).
|
||||
let returning_start = path
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| matches!(item.kind, MatchedKind::Word("returning")))
|
||||
.map(|item| item.span.0);
|
||||
let row_source = path
|
||||
.items
|
||||
.iter()
|
||||
@@ -901,7 +910,8 @@ fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
matches!(item.kind, MatchedKind::Word("values" | "select" | "with"))
|
||||
})
|
||||
.map(|item| {
|
||||
source[item.span.0..]
|
||||
let end = returning_start.unwrap_or(source.len());
|
||||
source[item.span.0..end]
|
||||
.trim()
|
||||
.trim_end_matches(';')
|
||||
.trim()
|
||||
@@ -920,9 +930,21 @@ fn build_sql_insert(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
target_table,
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning: path_has_returning(path),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether the matched path contains a `RETURNING` clause
|
||||
/// (ADR-0033 §5, sub-phase 3g). Located by the `returning` *Word
|
||||
/// token* in the path — path-based, so a string literal can't be
|
||||
/// mistaken for the keyword (mirrors `build_sql_insert`'s
|
||||
/// row-source detection).
|
||||
fn path_has_returning(path: &MatchedPath) -> bool {
|
||||
path.items
|
||||
.iter()
|
||||
.any(|item| matches!(item.kind, MatchedKind::Word("returning")))
|
||||
}
|
||||
|
||||
/// Build `Command::SqlUpdate` from a validated SQL `UPDATE`
|
||||
/// (ADR-0033 §2, sub-phase 3e). Extracts the target table from the
|
||||
/// matched path so the worker re-persists the right CSV.
|
||||
@@ -949,7 +971,11 @@ fn build_sql_update(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
.first()
|
||||
.map_or(source, |entry| &source[entry.span.1..]);
|
||||
let sql = format!("update {}", tail.trim());
|
||||
Ok(Command::SqlUpdate { sql, target_table })
|
||||
Ok(Command::SqlUpdate {
|
||||
sql,
|
||||
target_table,
|
||||
returning: path_has_returning(path),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build `Command::SqlDelete` from a validated SQL `DELETE`
|
||||
@@ -982,7 +1008,11 @@ fn build_sql_delete(path: &MatchedPath, source: &str) -> Result<Command, Validat
|
||||
.first()
|
||||
.map_or(source, |entry| &source[entry.span.1..]);
|
||||
let sql = format!("delete {}", tail.trim());
|
||||
Ok(Command::SqlDelete { sql, target_table })
|
||||
Ok(Command::SqlDelete {
|
||||
sql,
|
||||
target_table,
|
||||
returning: path_has_returning(path),
|
||||
})
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! later. The worker never inspects the WHERE clause (Amendment 2),
|
||||
//! so no predicate-byte extraction is needed.
|
||||
|
||||
use crate::dsl::grammar::sql_select::{WHERE_CLAUSE, reject_internal_table};
|
||||
use crate::dsl::grammar::sql_select::{RETURNING_CLAUSE, WHERE_CLAUSE, reject_internal_table};
|
||||
use crate::dsl::grammar::{IdentSource, Node, Word};
|
||||
|
||||
/// The `DELETE` target table. `__rdbms_*` rejected (ADR-0030 §6 /
|
||||
@@ -48,6 +48,7 @@ static SQL_DELETE_TAIL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("from")),
|
||||
TARGET_TABLE,
|
||||
Node::Optional(&WHERE_CLAUSE),
|
||||
Node::Optional(&RETURNING_CLAUSE),
|
||||
Node::Optional(&Node::Punct(';')),
|
||||
];
|
||||
|
||||
@@ -123,6 +124,14 @@ mod tests {
|
||||
good("from orders where customer_id in (select id from customers where country = 'DE')");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_tail_admitted() {
|
||||
// 3g: optional RETURNING projection_list tail.
|
||||
good("from orders where id = 1 returning *");
|
||||
good("from orders returning id, total");
|
||||
good("from orders where id = 1 returning id as gone;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_target_table_rejected() {
|
||||
bad("from __rdbms_playground_columns");
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -143,6 +143,11 @@ static EMPTY_NOMATCH: Node = Node::Choice(&[]);
|
||||
const PROJECTION_FOLLOW_SET: &[&str] = &[
|
||||
"from", "where", "group", "order", "having", "limit",
|
||||
"union", "intersect", "except",
|
||||
// `returning` belongs to an enclosing DML statement
|
||||
// (`INSERT … SELECT … RETURNING …`, ADR-0033 §5), never to a
|
||||
// projection item's bare alias — so a no-FROM SELECT row source
|
||||
// (`select id returning *`) stops before it.
|
||||
"returning",
|
||||
];
|
||||
|
||||
/// Continuation keywords that may legitimately follow a table
|
||||
@@ -156,6 +161,10 @@ const TABLE_SOURCE_FOLLOW_SET: &[&str] = &[
|
||||
"where", "group", "order", "having", "limit",
|
||||
"union", "intersect", "except",
|
||||
"inner", "left", "right", "full", "cross", "join", "on",
|
||||
// `returning` belongs to an enclosing DML statement
|
||||
// (`INSERT … SELECT … FROM t RETURNING …`, ADR-0033 §5), so the
|
||||
// SELECT row source must not read it as table `t`'s bare alias.
|
||||
"returning",
|
||||
];
|
||||
|
||||
fn peek_next_ident_lower(source: &str, pos: usize) -> Option<String> {
|
||||
@@ -325,12 +334,26 @@ fn projection_item_factory(
|
||||
|
||||
static PROJECTION_ITEM: Node = Node::Lookahead(projection_item_factory);
|
||||
|
||||
static PROJECTION_LIST: Node = Node::Repeated {
|
||||
pub(crate) static PROJECTION_LIST: Node = Node::Repeated {
|
||||
inner: &PROJECTION_ITEM,
|
||||
separator: Some(&COMMA),
|
||||
min: 1,
|
||||
};
|
||||
|
||||
/// `RETURNING projection_list` — the optional tail shared by the
|
||||
/// SQL DML statements (ADR-0033 §5, sub-phase 3g). Reuses the
|
||||
/// Phase-2 projection list unchanged (`*`, bare/qualified column
|
||||
/// refs, `expr AS alias`, computed expressions), so a RETURNING
|
||||
/// projection is parsed, completed and highlighted exactly as a
|
||||
/// SELECT projection. The worker collects the returned rows as a
|
||||
/// `DataResult`; result-column playground types are recovered via
|
||||
/// the same column-origin path SELECT uses (ADR-0032 §12).
|
||||
pub(crate) static RETURNING_CLAUSE: Node = Node::Seq(RETURNING_CLAUSE_NODES);
|
||||
static RETURNING_CLAUSE_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("returning")),
|
||||
Node::Subgrammar(&PROJECTION_LIST),
|
||||
];
|
||||
|
||||
// =================================================================
|
||||
// DISTINCT / ALL prefix
|
||||
// =================================================================
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! written (ADR-0030 §12). `RETURNING` (3g) lands later.
|
||||
|
||||
use crate::dsl::grammar::sql_expr;
|
||||
use crate::dsl::grammar::sql_select::{WHERE_CLAUSE, reject_internal_table};
|
||||
use crate::dsl::grammar::sql_select::{RETURNING_CLAUSE, WHERE_CLAUSE, reject_internal_table};
|
||||
use crate::dsl::grammar::{IdentSource, Node, Word};
|
||||
|
||||
static COMMA: Node = Node::Punct(',');
|
||||
@@ -79,6 +79,7 @@ static SQL_UPDATE_TAIL_NODES: &[Node] = &[
|
||||
Node::Word(Word::keyword("set")),
|
||||
ASSIGNMENT_LIST,
|
||||
Node::Optional(&WHERE_CLAUSE),
|
||||
Node::Optional(&RETURNING_CLAUSE),
|
||||
Node::Optional(&Node::Punct(';')),
|
||||
];
|
||||
|
||||
@@ -153,6 +154,14 @@ mod tests {
|
||||
good("t set v = (select max(other) from other_table) where id = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_tail_admitted() {
|
||||
// 3g: optional RETURNING projection_list tail.
|
||||
good("t set v = 1 where id = 1 returning *");
|
||||
good("t set v = 1 returning id, v");
|
||||
good("t set v = 1 where id = 1 returning v as new_v;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn internal_target_table_rejected() {
|
||||
bad("__rdbms_playground_columns set a = 1");
|
||||
|
||||
Reference in New Issue
Block a user