Skip to content

parser: support RETURNING clause syntax#65633

Merged
bb7133 merged 1 commit into
pingcap:masterfrom
bb7133:siddontang/returning-clause
May 19, 2026
Merged

parser: support RETURNING clause syntax#65633
bb7133 merged 1 commit into
pingcap:masterfrom
bb7133:siddontang/returning-clause

Conversation

@bb7133
Copy link
Copy Markdown
Member

@bb7133 bb7133 commented Jan 19, 2026

What problem does this PR solve?

Issue Number: ref #58939

Problem Summary:

This PR is the parser-only part of RETURNING support, split out after review feedback from @mjonss.

The executor/planner implementation has been moved to the follow-up stacked PR: bb7133#49

What changed and how does it work?

This PR only teaches the parser/AST layer to recognize and restore DML RETURNING syntax:

  • adds RETURNING grammar for INSERT / UPDATE / DELETE statements
  • stores the parsed field list in the corresponding AST nodes
  • keeps RETURNING as an unreserved keyword
  • regenerates parser.go / keywords.go
  • adds parser tests for INSERT / UPDATE / DELETE RETURNING syntax

No executor or planner behavior is included in this PR.

Check List

Tests

  • Unit test
  • Integration test
  • Manual test
  • No need to test
    • I checked and no code files have been changed.

Validation run:

  • make parser_yacc
  • cd pkg/parser && go test . -run 'TestDMLStmt|TestKeywords' -count=1
  • go test ./pkg/executor ./pkg/planner/core -run TestNonExistentReturningCompileOnly -count=1

Side effects

This is parser-only syntax support. Runtime execution support is intentionally kept out of this PR and moved to the follow-up stacked PR.

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

None

Summary by CodeRabbit

  • New Features

    • Optional RETURNING clauses for INSERT, UPDATE, and DELETE so statements can return specified columns/expressions.
  • Parser

    • Recognizes the RETURNING keyword and parses an optional returning field list; grammar updated to handle new clause without conflicts and to restore normalized SQL with RETURNING.
  • Tests

    • Added/extended tests validating RETURNING clause parsing and restored/normalized SQL output.

Review Change Stack

Copilot AI review requested due to automatic review settings January 19, 2026 06:32
@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. sig/planner SIG: Planner labels Jan 19, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for the RETURNING clause in DML statements, specifically implementing full support for INSERT ... RETURNING while parsing (but not executing) UPDATE/DELETE ... RETURNING. The RETURNING clause allows INSERT statements to return the values of affected rows immediately after insertion, similar to MariaDB's implementation.

Changes:

  • Added parser support for RETURNING clause syntax in INSERT, UPDATE, and DELETE statements
  • Implemented planner logic to build RETURNING expressions, schemas, and column names for INSERT statements
  • Added executor logic to capture and return inserted row data when RETURNING is specified
  • UPDATE/DELETE RETURNING are parsed but return "not supported yet" errors

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/parser/parser.y Added grammar rules for RETURNING clause in INSERT, UPDATE, and DELETE statements with proper precedence
pkg/parser/misc.go Added "RETURNING" keyword token mapping
pkg/parser/parser_test.go Added parser test cases for RETURNING clause syntax validation
pkg/parser/ast/dml.go Added Returning field to InsertStmt, UpdateStmt, and DeleteStmt AST nodes with Restore and Accept methods
pkg/planner/core/planbuilder.go Implemented buildReturningClause method to convert RETURNING AST to expressions and schemas for INSERT
pkg/planner/core/logical_plan_builder.go Added error handling to reject UPDATE/DELETE with RETURNING (not yet supported)
pkg/planner/core/point_get_plan.go Disabled point get optimization when RETURNING clause is present for UPDATE/DELETE
pkg/planner/core/operator/physicalop/physical_common_plans.go Added Returning, ReturningSchema, and ReturningNames fields to Insert, Update, and Delete structs
pkg/executor/builder.go Modified buildInsert to set RETURNING schema on base executor and pass RETURNING expressions to InsertExec
pkg/executor/insert.go Implemented RETURNING clause execution: appendReturningRow to capture rows, addRecord wrapper, and nextWithReturning to evaluate and return captured rows
tests/integrationtest/t/executor/returning.test Added comprehensive integration tests for INSERT RETURNING covering various scenarios
tests/integrationtest/r/executor/returning.result Expected results for RETURNING integration tests

Comment thread pkg/executor/insert.go
e.appendReturningRow(row)
return nil
}

Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addRecord wrapper only captures rows for RETURNING, but addRecordWithAutoIDHint is called directly at line 138 in the exec method, bypassing this wrapper. This means that for multi-row inserts, the first row of each shard batch (when i%sizeHintStep == 0) won't be captured in the RETURNING result, causing incomplete RETURNING output. You need to override addRecordWithAutoIDHint in InsertExec to also call appendReturningRow, similar to how addRecord is overridden.

Suggested change
func (e *InsertExec) addRecordWithAutoIDHint(ctx context.Context, row []types.Datum, dupKeyCheck table.DupKeyCheckMode) error {
if err := e.InsertValues.addRecordWithAutoIDHint(ctx, row, dupKeyCheck); err != nil {
return err
}
e.appendReturningRow(row)
return nil
}

Copilot uses AI. Check for mistakes.
@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from 8772840 to 68d79a4 Compare January 20, 2026 04:40
Copilot AI review requested due to automatic review settings January 20, 2026 18:45
@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from 68d79a4 to 68f1bb6 Compare January 20, 2026 18:45
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings January 20, 2026 21:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@hawkingrei hawkingrei force-pushed the siddontang/returning-clause branch from fc78c71 to ed4cca1 Compare January 21, 2026 01:38
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.8887%. Comparing base (4598d48) to head (da245c5).
⚠️ Report is 48 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #65633        +/-   ##
================================================
- Coverage   77.7123%   76.8887%   -0.8237%     
================================================
  Files          1991       2036        +45     
  Lines        552087     580288     +28201     
================================================
+ Hits         429040     446176     +17136     
- Misses       122127     131232      +9105     
- Partials        920       2880      +1960     
Flag Coverage Δ
integration 45.9602% <ø> (+6.1584%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 60.4888% <ø> (ø)
parser ∅ <ø> (∅)
br 64.8053% <ø> (+1.7118%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI review requested due to automatic review settings January 21, 2026 05:51
@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from ed4cca1 to fb7a95e Compare January 21, 2026 05:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from fb7a95e to e412c24 Compare January 21, 2026 19:14
@bb7133
Copy link
Copy Markdown
Member Author

bb7133 commented Jan 21, 2026

/retest

1 similar comment
@bb7133
Copy link
Copy Markdown
Member Author

bb7133 commented Jan 21, 2026

/retest

@mjonss
Copy link
Copy Markdown
Contributor

mjonss commented Apr 8, 2026

@bb7133 Is it possible to break this PR into several? Like one parser only PR, one for INSERT RETURNING etc.

If you need a reviewer, please assign me :)

@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from e412c24 to 7421823 Compare May 11, 2026 21:54
@bb7133
Copy link
Copy Markdown
Member Author

bb7133 commented May 11, 2026

@mjonss Yes, splitting this into a parser-only precursor plus the INSERT RETURNING implementation makes sense. I have refreshed the branch against latest master first so the PR is no longer conflicting. If you prefer that review shape, I can extract the parser/AST/generated-parser pieces into a separate PR and keep this one focused on the executor/planner INSERT RETURNING work.

I will also add you as a reviewer.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds SQL RETURNING clause parsing support to the TiDB parser for INSERT, UPDATE, and DELETE: registers the RETURNING keyword/token, adds Returning []*SelectField to DML AST nodes (restore and visitor traversal), updates grammar with ReturningClause and precedence fixes, and adds parser tests for RETURNING forms.

Changes

SQL Parser RETURNING Clause Implementation

Layer / File(s) Summary
Keyword and Token Registration
pkg/parser/keywords.go, pkg/parser/misc.go, pkg/parser/keywords_test.go
RETURNING is registered as an unreserved keyword in the Keywords list and token map; keyword-count test updated from 679 to 680.
Parser Declarations & Precedence
pkg/parser/parser.y
Adds RETURNING token, declares ReturningClause and TableAsNameOptDelete, introduces %precedence returning and %precedence higherThanReturning, and updates empty alternatives' precedence to avoid shift/reduce conflicts.
DML Rules and RETURNING Integration
pkg/parser/parser.y
Extends DeleteWithoutUsingStmt, InsertIntoStmt, and UpdateStmtNoWith to accept optional ReturningClause; assigns parsed *ast.FieldList (list of SelectField) to statements' Returning fields; defines ReturningClause parsing and adds literal "RETURNING" to unreserved list.
DML AST Node Extensions
pkg/parser/ast/dml.go
InsertStmt, DeleteStmt, and UpdateStmt each gain a Returning []*SelectField field; Restore methods emit RETURNING <field-list> when present using a new helper; Accept methods traverse and replace the Returning nodes.
Parser Test Coverage
pkg/parser/parser_test.go
TestDMLStmt extended with cases for INSERT, UPDATE, and DELETE using RETURNING (including RETURNING *, single/multiple expressions, ON DUPLICATE KEY UPDATE interaction, and ORDER BY ... LIMIT ... RETURNING).

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels: ok-to-test

Suggested reviewers:

  • hawkingrei
  • tangenta
  • yudongusa
  • benmeadowcroft

🐰 I hopped through grammar, keyword, and AST,
RETURNING now sings where DML once passed,
Fields are collected, restored in their place,
Tests peek and nod at each returned case,
A tiny rabbit dance — parser updated, fast!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and concisely describes the main change: adding RETURNING clause syntax support to the parser.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is complete and well-structured, covering problem statement, changes made, test validation, side effects, and documentation impact.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bb7133 bb7133 requested a review from mjonss May 11, 2026 21:55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/parser/parser.y (1)

6915-6920: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Scope the RETURNING precedence hack to RETURNING lists only.

Line 6915 changes the shared FieldAsNameOpt empty arm to outrank the RETURNING token, so ordinary SELECT field lists can no longer use a bare alias named returning (for example, SELECT a returning FROM t). This should be isolated to a RETURNING-specific field/alias nonterminal instead of changing global SELECT parsing behavior.

Suggested direction
-FieldAsNameOpt:
-	/* EMPTY */ %prec higherThanReturning
+FieldAsNameOpt:
+	/* EMPTY */ %prec empty
 	{
 		$$ = ""
 	}
 |	FieldAsName

+ReturningFieldAsNameOpt:
+	/* EMPTY */ %prec higherThanReturning
+	{
+		$$ = ""
+	}
+|	FieldAsName
+
+ReturningField:
+	'*' %prec '*'
+	{
+		$$ = &ast.SelectField{WildCard: &ast.WildCardField{}}
+	}
+|	Identifier '.' '*' %prec '*'
+	{
+		wildCard := &ast.WildCardField{Table: ast.NewCIStr($1)}
+		$$ = &ast.SelectField{WildCard: wildCard}
+	}
+|	Identifier '.' Identifier '.' '*' %prec '*'
+	{
+		wildCard := &ast.WildCardField{Schema: ast.NewCIStr($1), Table: ast.NewCIStr($3)}
+		$$ = &ast.SelectField{WildCard: wildCard}
+	}
+|	Expression ReturningFieldAsNameOpt
+	{
+		$$ = &ast.SelectField{Expr: $1, AsName: ast.NewCIStr($2)}
+	}
+
+ReturningFieldList:
+	ReturningField
+	{
+		$$ = []*ast.SelectField{$1.(*ast.SelectField)}
+	}
+|	ReturningFieldList ',' ReturningField
+	{
+		$$ = append($1.([]*ast.SelectField), $3.(*ast.SelectField))
+	}
+
 ReturningClause:
 	%prec empty
 	{
 		$$ = nil
 	}
-|	"RETURNING" FieldList
+|	"RETURNING" ReturningFieldList
 	{
 		$$ = &ast.FieldList{Fields: $2.([]*ast.SelectField)}
 	}

You'd also need matching %type entries for the new nonterminals.

Also applies to: 8053-8061

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/parser/parser.y` around lines 6915 - 6920, The change made
FieldAsNameOpt's empty alternative to use %prec higherThanReturning, which
unintentionally affects all SELECT field lists and prevents using an alias named
"returning"; revert that global precedence change and instead create a
RETURNING-scoped nonterminal (e.g., FieldAsNameOptReturning and related
FieldAsNameReturning) whose empty arm uses %prec higherThanReturning, update the
RETURNING-list productions to use these new nonterminals, and add matching %type
declarations for the new nonterminals; apply the same scoping fix to the other
occurrence referenced (the block around the second instance similar to
8053-8061).
🧹 Nitpick comments (2)
pkg/parser/parser_test.go (1)

920-931: ⚡ Quick win

Add parser/restore cases for the remaining INSERT RETURNING forms.

Lines 920-931 cover VALUES and ON DUPLICATE KEY UPDATE, but they don’t include INSERT ... SELECT, INSERT ... SET, and INSERT IGNORE ... RETURNING, which are part of this PR’s scope. Adding them here would better lock the grammar/restore branches.

🧪 Proposed test additions
 		{"INSERT INTO t (a) VALUES (1) RETURNING id, name", true, "INSERT INTO `t` (`a`) VALUES (1) RETURNING `id`, `name`"},
+		{"INSERT IGNORE INTO t (a) VALUES (1) RETURNING id", true, "INSERT IGNORE INTO `t` (`a`) VALUES (1) RETURNING `id`"},
+		{"INSERT INTO t SET a=1 RETURNING a", true, "INSERT INTO `t` SET `a`=1 RETURNING `a`"},
+		{"INSERT INTO t (a) SELECT a FROM s RETURNING a", true, "INSERT INTO `t` (`a`) SELECT `a` FROM `s` RETURNING `a`"},
 		{"INSERT INTO t (a) VALUES (1) ON DUPLICATE KEY UPDATE a=2 RETURNING id", true, "INSERT INTO `t` (`a`) VALUES (1) ON DUPLICATE KEY UPDATE `a`=2 RETURNING `id`"},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/parser/parser_test.go` around lines 920 - 931, The test table in
parser_test.go is missing cases for INSERT ... SELECT, INSERT ... SET, and
INSERT IGNORE ... RETURNING; add new entries to the existing test slice (the
same block with other RETURNING cases) that assert correct restore strings for
these forms — e.g. add a case for "INSERT INTO t (a) SELECT b FROM s RETURNING
id" expecting "INSERT INTO `t` (`a`) SELECT `b` FROM `s` RETURNING `id`", a case
for "INSERT INTO t SET a=1 RETURNING id" expecting "INSERT INTO `t` SET `a`=1
RETURNING `id`", and a case for "INSERT IGNORE INTO t (a) VALUES (1) RETURNING
*" expecting "INSERT IGNORE INTO `t` (`a`) VALUES (1) RETURNING *"; place them
alongside the existing RETURNING test rows so the parser/restore branches are
exercised.
pkg/planner/core/logical_plan_builder.go (1)

6227-6230: ⚡ Quick win

Fail fast for unsupported RETURNING before building/optimizing DML plans.

Both guards are currently placed after expensive work (BuildOnUpdateFKTriggers / DoOptimize). Since these paths always return ErrNotSupportedYet, reject earlier in buildUpdate/buildDelete (right after AST-level validation) to avoid unnecessary planning overhead and side effects (extra warnings/stats-load interactions).

💡 Suggested adjustment
 func (b *PlanBuilder) buildUpdate(ctx context.Context, update *ast.UpdateStmt) (base.Plan, error) {
+    if update.Returning != nil {
+        return nil, plannererrors.ErrNotSupportedYet.GenWithStackByArgs("RETURNING clause")
+    }
+
     b.pushSelectOffset(0)
     b.pushTableHints(update.TableHints, 0)
     ...
-    // Handle RETURNING clause
-    if update.Returning != nil {
-        return nil, plannererrors.ErrNotSupportedYet.GenWithStackByArgs("RETURNING clause")
-    }
-
     return updt, nil
 }

 func (b *PlanBuilder) buildDelete(ctx context.Context, ds *ast.DeleteStmt) (base.Plan, error) {
+    if ds.Returning != nil && len(ds.Returning.Fields) > 0 {
+        return nil, plannererrors.ErrNotSupportedYet.GenWithStackByArgs("RETURNING clause")
+    }
+
     b.pushSelectOffset(0)
     b.pushTableHints(ds.TableHints, 0)
     ...
-    // Handle RETURNING clause
-    if ds.Returning != nil && len(ds.Returning.Fields) > 0 {
-        return nil, plannererrors.ErrNotSupportedYet.GenWithStackByArgs("RETURNING clause")
-    }
-
     return del, nil
 }

Also applies to: 6703-6705

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/logical_plan_builder.go` around lines 6227 - 6230, Reject
unsupported RETURNING earlier: in buildUpdate and buildDelete, immediately after
AST-level validation check for update.Returning != nil (or delete.Returning) and
return plannererrors.ErrNotSupportedYet.GenWithStackByArgs("RETURNING clause")
there, instead of waiting until later after BuildOnUpdateFKTriggers or
DoOptimize; this avoids running BuildOnUpdateFKTriggers, DoOptimize and other
expensive/side-effecting work when RETURNING is always unsupported.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/executor/insert.go`:
- Around line 57-61: The code currently accumulates all RETURNING rows in
returningRows and only emits after the entire INSERT finishes; change this to
stream RETURNING rows incrementally across Next calls by evaluating
returningExprs per affected row and producing chunks as you go, preserving
executor state between Next invocations. Modify the InsertExec state so instead
of populating returningRows you iterate the child/source rows inside Next (or a
helper like fetchChildRows) and for each affected row evaluate returningExprs
against returningSchema, append into the current output chunk, and return the
chunk when it reaches the executor chunk size or when the source is exhausted;
set returningDone only when no more rows remain and avoid retaining the full
result set in memory. Ensure you update/replace uses of
returningRows/returningDone in InsertExec, keep returningExprs and
returningSchema for per-row evaluation, and handle cancellation/errors exactly
as before while freeing any per-row temporary state as soon as it’s emitted.

In `@pkg/planner/core/operator/physicalop/physical_common_plans.go`:
- Around line 100-103: The MemoryUsage implementations for Insert, Update, and
Delete need to account for the newly added Returning, ReturningSchema, and
ReturningNames fields: update the MemoryUsage methods for the Insert, Update,
and Delete types to add the memory contributed by the Returning slice (include
slice header plus each expression's memory via its MemoryUsage or a conservative
per-element size), add the ReturningSchema reference memory (use
ReturningSchema.MemoryUsage() if available or include its structural size), and
add the ReturningNames slice memory (slice header plus names' bytes). Locate the
MemoryUsage methods named MemoryUsage on the Insert, Update, and Delete types
and add these three contributions using the same sizing approach/pattern used
elsewhere in the file for other slices/Schema/NameSlice to keep calculations
consistent.

In `@pkg/planner/core/planbuilder.go`:
- Around line 6462-6469: When handling field.WildCard in the RETURNING
expansion, first check whether the wildcard is qualified
(field.WildCard.Qualifier non-nil); if it is, only expand columns for the
current table when the qualifier matches this table's identifier or alias
(compare against the current table's name/alias/entry you use when building
tableNames), otherwise return an unknown-table error instead of expanding
tableSchema.Columns into exprs/cols/names. Ensure the unqualified case still
expands all visible columns as before.
- Around line 6474-6478: The rewritten plan returned from b.rewrite (np) is
being discarded for RETURNING expressions which breaks expressions that require
extra plan state (e.g., scalar subqueries); update the RETURNING handling in
planbuilder.go to check the returned np: if np != mockPlan then either
preserve/attach np to the surrounding plan node or explicitly reject the
expression by returning an error (e.g., "RETURNING does not support
non-row-local expressions"); specifically, change the code around the call expr,
np, err := b.rewrite(ctx, field.Expr, mockPlan, nil, true) to handle non-dual np
instead of unconditionally ignoring np so that expressions requiring subplans
are not lost.
- Around line 4199-4211: The INSERT branch that handles a RETURNING clause (in
buildInsert where buildReturningClause is called and
insertPlan.SetSchema/SetOutputNames are set) must also require SELECT privilege;
update the insert plan's required privileges so that when insert.Returning !=
nil and len(insert.Returning.Fields) > 0 you add the SELECT privilege (in
addition to existing INSERT/UPDATE/DELETE privileges) to insertPlan (e.g., via
the same mechanism used to record other privileges on the plan) before returning
the plan, ensuring SELECT is enforced when RETURNING is present.

---

Outside diff comments:
In `@pkg/parser/parser.y`:
- Around line 6915-6920: The change made FieldAsNameOpt's empty alternative to
use %prec higherThanReturning, which unintentionally affects all SELECT field
lists and prevents using an alias named "returning"; revert that global
precedence change and instead create a RETURNING-scoped nonterminal (e.g.,
FieldAsNameOptReturning and related FieldAsNameReturning) whose empty arm uses
%prec higherThanReturning, update the RETURNING-list productions to use these
new nonterminals, and add matching %type declarations for the new nonterminals;
apply the same scoping fix to the other occurrence referenced (the block around
the second instance similar to 8053-8061).

---

Nitpick comments:
In `@pkg/parser/parser_test.go`:
- Around line 920-931: The test table in parser_test.go is missing cases for
INSERT ... SELECT, INSERT ... SET, and INSERT IGNORE ... RETURNING; add new
entries to the existing test slice (the same block with other RETURNING cases)
that assert correct restore strings for these forms — e.g. add a case for
"INSERT INTO t (a) SELECT b FROM s RETURNING id" expecting "INSERT INTO `t`
(`a`) SELECT `b` FROM `s` RETURNING `id`", a case for "INSERT INTO t SET a=1
RETURNING id" expecting "INSERT INTO `t` SET `a`=1 RETURNING `id`", and a case
for "INSERT IGNORE INTO t (a) VALUES (1) RETURNING *" expecting "INSERT IGNORE
INTO `t` (`a`) VALUES (1) RETURNING *"; place them alongside the existing
RETURNING test rows so the parser/restore branches are exercised.

In `@pkg/planner/core/logical_plan_builder.go`:
- Around line 6227-6230: Reject unsupported RETURNING earlier: in buildUpdate
and buildDelete, immediately after AST-level validation check for
update.Returning != nil (or delete.Returning) and return
plannererrors.ErrNotSupportedYet.GenWithStackByArgs("RETURNING clause") there,
instead of waiting until later after BuildOnUpdateFKTriggers or DoOptimize; this
avoids running BuildOnUpdateFKTriggers, DoOptimize and other
expensive/side-effecting work when RETURNING is always unsupported.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c0522b19-8e61-4556-8407-ed69045c4205

📥 Commits

Reviewing files that changed from the base of the PR and between 4598d48 and 7421823.

📒 Files selected for processing (16)
  • pkg/executor/builder.go
  • pkg/executor/insert.go
  • pkg/parser/ast/dml.go
  • pkg/parser/keywords.go
  • pkg/parser/keywords_test.go
  • pkg/parser/misc.go
  • pkg/parser/parser.go
  • pkg/parser/parser.y
  • pkg/parser/parser_test.go
  • pkg/planner/core/logical_plan_builder.go
  • pkg/planner/core/operator/physicalop/physical_common_plans.go
  • pkg/planner/core/operator/physicalop/plan_clone_generated.go
  • pkg/planner/core/planbuilder.go
  • pkg/planner/core/point_get_plan.go
  • tests/integrationtest/r/executor/returning.result
  • tests/integrationtest/t/executor/returning.test

Comment thread pkg/executor/insert.go Outdated
Comment on lines +57 to +61
// For RETURNING clause support
returningExprs []expression.Expression
returningSchema *expression.Schema
returningRows [][]types.Datum
returningDone bool
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid full materialization of RETURNING rows before emission.

The current flow buffers every affected row in returningRows and only emits after the whole insert finishes. For large INSERT ... SELECT ... RETURNING, this can cause unbounded memory growth and high latency before any rows are returned.

Please switch to chunked/streamed RETURNING production (emit incrementally and keep executor state across Next calls) instead of collecting the full result set first.

Also applies to: 64-73, 422-466

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/executor/insert.go` around lines 57 - 61, The code currently accumulates
all RETURNING rows in returningRows and only emits after the entire INSERT
finishes; change this to stream RETURNING rows incrementally across Next calls
by evaluating returningExprs per affected row and producing chunks as you go,
preserving executor state between Next invocations. Modify the InsertExec state
so instead of populating returningRows you iterate the child/source rows inside
Next (or a helper like fetchChildRows) and for each affected row evaluate
returningExprs against returningSchema, append into the current output chunk,
and return the chunk when it reaches the executor chunk size or when the source
is exhausted; set returningDone only when no more rows remain and avoid
retaining the full result set in memory. Ensure you update/replace uses of
returningRows/returningDone in InsertExec, keep returningExprs and
returningSchema for per-row evaluation, and handle cancellation/errors exactly
as before while freeing any per-row temporary state as soon as it’s emitted.

Comment on lines +100 to +103
// Returning stores the RETURNING clause expression list.
Returning []expression.Expression
ReturningSchema *expression.Schema `plan-cache-clone:"shallow"`
ReturningNames types.NameSlice `plan-cache-clone:"shallow"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update MemoryUsage() to include newly added RETURNING fields.

The new Returning* fields are added to Insert, Update, and Delete, but their corresponding MemoryUsage() methods still exclude those slices/schema/name memory. This under-accounts plan memory and can skew memory-control behavior.

💡 Suggested fix (pattern)
 // Insert.MemoryUsage
- sum = ... + size.SizeOfSlice*7 + ...
+ sum = ... + size.SizeOfSlice*9 + ...
+ sum += int64(cap(p.Returning))*size.SizeOfInterface
+ sum += int64(cap(p.ReturningNames))*size.SizeOfPointer
+ if p.ReturningSchema != nil {
+   sum += p.ReturningSchema.MemoryUsage()
+ }
+ for _, expr := range p.Returning {
+   sum += expr.MemoryUsage()
+ }
+ for _, name := range p.ReturningNames {
+   sum += name.MemoryUsage()
+ }

 // Update.MemoryUsage
- sum = ... + size.SizeOfSlice*3 + ...
+ sum = ... + size.SizeOfSlice*5 + ...
+ sum += int64(cap(p.Returning))*size.SizeOfInterface
+ sum += int64(cap(p.ReturningNames))*size.SizeOfPointer
+ if p.ReturningSchema != nil {
+   sum += p.ReturningSchema.MemoryUsage()
+ }
+ for _, expr := range p.Returning {
+   sum += expr.MemoryUsage()
+ }
+ for _, name := range p.ReturningNames {
+   sum += name.MemoryUsage()
+ }

 // Delete.MemoryUsage
- sum = ... + size.SizeOfSlice
+ sum = ... + size.SizeOfSlice*3
+ sum += int64(cap(p.Returning))*size.SizeOfInterface
+ sum += int64(cap(p.ReturningNames))*size.SizeOfPointer
+ if p.ReturningSchema != nil {
+   sum += p.ReturningSchema.MemoryUsage()
+ }
+ for _, expr := range p.Returning {
+   sum += expr.MemoryUsage()
+ }
+ for _, name := range p.ReturningNames {
+   sum += name.MemoryUsage()
+ }

Also applies to: 181-184, 236-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/operator/physicalop/physical_common_plans.go` around lines
100 - 103, The MemoryUsage implementations for Insert, Update, and Delete need
to account for the newly added Returning, ReturningSchema, and ReturningNames
fields: update the MemoryUsage methods for the Insert, Update, and Delete types
to add the memory contributed by the Returning slice (include slice header plus
each expression's memory via its MemoryUsage or a conservative per-element
size), add the ReturningSchema reference memory (use
ReturningSchema.MemoryUsage() if available or include its structural size), and
add the ReturningNames slice memory (slice header plus names' bytes). Locate the
MemoryUsage methods named MemoryUsage on the Insert, Update, and Delete types
and add these three contributions using the same sizing approach/pattern used
elsewhere in the file for other slices/Schema/NameSlice to keep calculations
consistent.

Comment thread pkg/planner/core/planbuilder.go Outdated
Comment on lines +4199 to +4211
// Handle RETURNING clause
if insert.Returning != nil && len(insert.Returning.Fields) > 0 {
returningExprs, returningSchema, returningNames, err := b.buildReturningClause(
ctx, insert.Returning, insertPlan.TableSchema, insertPlan.TableColNames, mockTablePlan)
if err != nil {
return nil, err
}
insertPlan.Returning = returningExprs
insertPlan.ReturningSchema = returningSchema
insertPlan.ReturningNames = returningNames
// Set the output schema for the insert plan to be the returning schema
insertPlan.SetSchema(returningSchema)
insertPlan.SetOutputNames(returningNames)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Require SELECT privilege when RETURNING is present.

This branch makes INSERT return row data, but buildInsert still only records INSERT/DELETE/UPDATE privileges. That lets statements like INSERT ... ON DUPLICATE KEY UPDATE ... RETURNING sensitive_col expose existing row values without any SELECT check.

Suggested fix
 	// Handle RETURNING clause
 	if insert.Returning != nil && len(insert.Returning.Fields) > 0 {
+		var selectErr error
+		if user != nil {
+			selectErr = plannererrors.ErrTableaccessDenied.FastGenByArgs("SELECT", user.AuthUsername, user.AuthHostname, tableInfo.Name.L)
+		}
+		b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, tnW.DBInfo.Name.L, tableInfo.Name.L, "", selectErr)
+
 		returningExprs, returningSchema, returningNames, err := b.buildReturningClause(
 			ctx, insert.Returning, insertPlan.TableSchema, insertPlan.TableColNames, mockTablePlan)
 		if err != nil {
 			return nil, err
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle RETURNING clause
if insert.Returning != nil && len(insert.Returning.Fields) > 0 {
returningExprs, returningSchema, returningNames, err := b.buildReturningClause(
ctx, insert.Returning, insertPlan.TableSchema, insertPlan.TableColNames, mockTablePlan)
if err != nil {
return nil, err
}
insertPlan.Returning = returningExprs
insertPlan.ReturningSchema = returningSchema
insertPlan.ReturningNames = returningNames
// Set the output schema for the insert plan to be the returning schema
insertPlan.SetSchema(returningSchema)
insertPlan.SetOutputNames(returningNames)
// Handle RETURNING clause
if insert.Returning != nil && len(insert.Returning.Fields) > 0 {
var selectErr error
if user != nil {
selectErr = plannererrors.ErrTableaccessDenied.FastGenByArgs("SELECT", user.AuthUsername, user.AuthHostname, tableInfo.Name.L)
}
b.visitInfo = appendVisitInfo(b.visitInfo, mysql.SelectPriv, tnW.DBInfo.Name.L, tableInfo.Name.L, "", selectErr)
returningExprs, returningSchema, returningNames, err := b.buildReturningClause(
ctx, insert.Returning, insertPlan.TableSchema, insertPlan.TableColNames, mockTablePlan)
if err != nil {
return nil, err
}
insertPlan.Returning = returningExprs
insertPlan.ReturningSchema = returningSchema
insertPlan.ReturningNames = returningNames
// Set the output schema for the insert plan to be the returning schema
insertPlan.SetSchema(returningSchema)
insertPlan.SetOutputNames(returningNames)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/planbuilder.go` around lines 4199 - 4211, The INSERT branch
that handles a RETURNING clause (in buildInsert where buildReturningClause is
called and insertPlan.SetSchema/SetOutputNames are set) must also require SELECT
privilege; update the insert plan's required privileges so that when
insert.Returning != nil and len(insert.Returning.Fields) > 0 you add the SELECT
privilege (in addition to existing INSERT/UPDATE/DELETE privileges) to
insertPlan (e.g., via the same mechanism used to record other privileges on the
plan) before returning the plan, ensuring SELECT is enforced when RETURNING is
present.

Comment thread pkg/planner/core/planbuilder.go Outdated
Comment on lines +6462 to +6469
// Handle RETURNING *
if field.WildCard != nil {
// Expand * to all visible columns
for i, col := range tableSchema.Columns {
exprs = append(exprs, col)
cols = append(cols, col)
names = append(names, tableNames[i])
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate qualified wildcards before expanding them.

This treats every wildcard as bare *. RETURNING other_table.* or a mismatched schema-qualified wildcard would incorrectly expand the target table instead of raising an unknown-table error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/planbuilder.go` around lines 6462 - 6469, When handling
field.WildCard in the RETURNING expansion, first check whether the wildcard is
qualified (field.WildCard.Qualifier non-nil); if it is, only expand columns for
the current table when the qualifier matches this table's identifier or alias
(compare against the current table's name/alias/entry you use when building
tableNames), otherwise return an unknown-table error instead of expanding
tableSchema.Columns into exprs/cols/names. Ensure the unqualified case still
expands all visible columns as before.

Comment thread pkg/planner/core/planbuilder.go Outdated
Comment on lines +6474 to +6478
expr, np, err := b.rewrite(ctx, field.Expr, mockPlan, nil, true)
if err != nil {
return nil, nil, nil, err
}
_ = np // We don't need the new plan for simple column references
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't discard the rewritten plan for RETURNING expressions.

rewrite only returns a different np when the expression needs extra plan state, e.g. scalar subqueries. Ignoring it here means those expressions are accepted without preserving the subplan, so they'll fail or mis-evaluate later. If RETURNING is only meant to allow row-local expressions for now, reject non-dual np explicitly.

Suggested fix
 		expr, np, err := b.rewrite(ctx, field.Expr, mockPlan, nil, true)
 		if err != nil {
 			return nil, nil, nil, err
 		}
-		_ = np // We don't need the new plan for simple column references
+		if _, ok := np.(*logicalop.LogicalTableDual); !ok {
+			return nil, nil, nil, errors.New("RETURNING doesn't support subqueries or other complex expressions yet")
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/planner/core/planbuilder.go` around lines 6474 - 6478, The rewritten plan
returned from b.rewrite (np) is being discarded for RETURNING expressions which
breaks expressions that require extra plan state (e.g., scalar subqueries);
update the RETURNING handling in planbuilder.go to check the returned np: if np
!= mockPlan then either preserve/attach np to the surrounding plan node or
explicitly reject the expression by returning an error (e.g., "RETURNING does
not support non-row-local expressions"); specifically, change the code around
the call expr, np, err := b.rewrite(ctx, field.Expr, mockPlan, nil, true) to
handle non-dual np instead of unconditionally ignoring np so that expressions
requiring subplans are not lost.

@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from 7421823 to fe20806 Compare May 11, 2026 23:21
@pingcap-cla-assistant
Copy link
Copy Markdown

pingcap-cla-assistant Bot commented May 11, 2026

CLA assistant check
All committers have signed the CLA.

@bb7133 bb7133 changed the title *: support returning clause for insert statement parser: support RETURNING clause syntax May 11, 2026
@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-tests-checked release-note-none Denotes a PR that doesn't merit a release note. and removed release-note Denotes a PR that will be considered when it comes time to generate release notes. labels May 11, 2026
@bb7133
Copy link
Copy Markdown
Member Author

bb7133 commented May 11, 2026

@mjonss The split is done now.

  • This PR is now parser-only: AST + grammar + generated parser/keywords + parser tests.
  • The executor/planner/integration-test implementation was moved to the stacked follow-up PR: executor: support INSERT RETURNING bb7133/tidb#49
  • I kept you requested as reviewer on this parser PR. GitHub does not allow requesting you on the fork-local stacked PR unless you are a collaborator on bb7133/tidb, so I linked it here instead.

@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from fe20806 to 5366eba Compare May 12, 2026 02:01
@bb7133
Copy link
Copy Markdown
Member Author

bb7133 commented May 12, 2026

Updated both split PRs to align RETURNING with MariaDB-style select_expr [, select_expr ...].

Changes:

  • Parser PR now parses RETURNING using SelectStmtFieldList, not the narrower raw FieldList, and includes a parser test for:
    RETURNING id,id+id,id&id,id||id
  • Implementation PR adds an integration test for the same MariaDB-style expression list and uses the existing projection field-name builder for expression result names.

Updated heads:

@bb7133 bb7133 force-pushed the siddontang/returning-clause branch 2 times, most recently from 9a4e0c1 to bcd9b0a Compare May 12, 2026 06:16
Copy link
Copy Markdown
Contributor

@mjonss mjonss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ti-chi-bot ti-chi-bot Bot added the needs-1-more-lgtm Indicates a PR needs 1 more LGTM. label May 12, 2026
@ti-chi-bot ti-chi-bot Bot added lgtm and removed needs-1-more-lgtm Indicates a PR needs 1 more LGTM. labels May 14, 2026
@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented May 14, 2026

[LGTM Timeline notifier]

Timeline:

  • 2026-05-12 23:50:43.960079572 +0000 UTC m=+223212.492858911: ☑️ agreed by mjonss.
  • 2026-05-14 13:16:33.438434745 +0000 UTC m=+357961.971214084: ☑️ agreed by tiancaiamao.

@bb7133 bb7133 force-pushed the siddontang/returning-clause branch from bcd9b0a to da245c5 Compare May 18, 2026 02:40
Copy link
Copy Markdown

@yudongusa yudongusa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please open a document PR and link to this

@D3Hunter
Copy link
Copy Markdown
Contributor

/approve

@ti-chi-bot
Copy link
Copy Markdown

ti-chi-bot Bot commented May 19, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: D3Hunter, mjonss, tiancaiamao, xhebox, yudongusa

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@bb7133
Copy link
Copy Markdown
Member Author

bb7133 commented May 19, 2026

/retest

@bb7133 bb7133 merged commit 5adafaf into pingcap:master May 19, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved lgtm release-note-none Denotes a PR that doesn't merit a release note. sig/planner SIG: Planner size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants