Skip to content

feat: support PERFORM SELECT statements - #26601

Merged
XuPeng-SH merged 7 commits into
matrixorigin:mainfrom
VioletQwQ-0:codex/issue-13060-perform
Aug 3, 2026
Merged

feat: support PERFORM SELECT statements#26601
XuPeng-SH merged 7 commits into
matrixorigin:mainfrom
VioletQwQ-0:codex/issue-13060-perform

Conversation

@VioletQwQ-0

@VioletQwQ-0 VioletQwQ-0 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What type of PR is this?

  • API-change
  • BUG
  • Improvement
  • Documentation
  • Feature
  • Test and CI
  • Code Refactoring

Which issue(s) this PR fixes:

issue #13060

What this PR does / why we need it:

Adds v1 support for PERFORM SELECT ... as a non-reserved SQL prefix.

  • Parses, formats, prepares, plans, authorizes, and executes the wrapped query through the existing SELECT path.
  • Executes the complete pipeline but suppresses result-set metadata and rows on the MySQL wire; successful COM_QUERY/COM_STMT_EXECUTE returns one OK response with affected rows normalized to zero.
  • Preserves query-result saving so last_query_id(), result_scan, and meta_scan can inspect saved PERFORM output, while failed or cancelled statements finalize saved-query state as failed.
  • Suppresses PERFORM result batches in background/procedure callbacks and preserves the normal status-response contract for SIDECAR requests.
  • Keeps parameter metadata while reporting zero result columns for prepared statements, including reprepare.
  • Rejects PERFORM SELECT ... INTO OUTFILE, including when the export is nested in a UNION branch, CTE body, or expression subquery, with not supported: PERFORM SELECT INTO OUTFILE.
  • Restricts the grammar to query-form SELECT statements and keeps perform non-reserved for existing table, column, and alias identifiers.
  • Classifies PERFORM as a SELECT statement in statement tracing.

Non-goals: PostgreSQL expression-form PERFORM/FOUND semantics, the PERFROM typo alias, non-SELECT statements, and changes to MySQL DO.

QA Decision

Required.

Reason: this changes user-visible SQL syntax, MySQL response shape, permission behavior, prepared-statement metadata, stored-procedure output, and query-result behavior.

Production entrypoint:

COM_QUERY / COM_STMT_PREPARE / COM_STMT_EXECUTE -> parser -> SELECT planner -> CN pipeline -> query-result saver -> OK response

Exact-head evidence

  • Comparison base: 298dc63bbc6707289dcf877563e3a3fc11113ba9
  • Head: 58b741a2aecd9cddce4a1c5faf54ad99aed56b3b
  • Exact-head preflight: PASS review=PASS validation=PENDING
  • Diff hash: 7a46804454fd3f69ebd296678bf8f185337ce0636d8a1aa959e3fea277af844b

Local validation on this exact head:

  • Parser-to-planner direct, CTE, projection-subquery, and predicate-subquery rejection plus nearest controls: PASS
  • .agents/skills/mo-dev/scripts/mo-cgo-test ./pkg/sql/plan -count=1: PASS
  • git diff --check: PASS

Earlier validation on prior heads remains supporting historical evidence and was not rerun after the latest nested-export fix:

  • Complete parser/tree/planner/frontend package tests and race tests
  • make build
  • Canonical BVT: perform.sql 47/47, do.sql 2/2, query_result.sql 196/196

GitHub CI for the current exact head is pending and remains an independent gate.

QA scenarios

  • MySQL client sees only OK for PERFORM, including zero-row and large-result queries.
  • Normal SELECT and MySQL DO behavior do not regress.
  • save_query_result, last_query_id(), result_scan, and meta_scan, including failure/cancellation cleanup.
  • Successful ROW_COUNT() = 0; failed PERFORM records ROW_COUNT() = -1.
  • Permission deny/grant and revoke-after-prepare behavior matches SELECT.
  • Text and binary prepare, including schema-triggered reprepare, report zero result columns.
  • Multi-statement response ordering and SERVER_MORE_RESULTS_EXISTS.
  • Stored procedure/background execution does not expose the inner result set.
  • Runtime error, saver error, kill, and cancel never produce a false OK or complete result metadata.
  • Top-level, UNION-nested, CTE-nested, and expression-subquery SELECT ... INTO OUTFILE are rejected before execution.

Keep issue #13060 open after merge. Transfer it to a confirmed tester with phase/testing; close only after terminal QA PASS evidence records the tested version/SHA, environment, scenarios, and regression result.

@matrix-meow matrix-meow added the size/XXL Denotes a PR that changes 2000+ lines label Aug 3, 2026
@VioletQwQ-0
VioletQwQ-0 marked this pull request as ready for review August 3, 2026 06:03
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@iamlinjunhong iamlinjunhong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings
[P1] 失败的 PERFORM 会污染下一条 saved query
PERFORM 把 batch 写入 query-result saver,但只有 runner.Run() 成功后才显式 finalize;runner 一旦报错会直接返回。status_stmt.go L44-L53
saveBatch 在真正创建/写入对象前就增加 queryRowCount 和 blockIdx;writer 创建、写 batch 或 WriteEnd 均可能失败。query_result.go L111-L145 失败时 pipeline 的 Output.Reset 不调用 terminal callback,output/types.go L131-L134,而 blockIdx、大小和行数只在 saveMeta 的 defer 中重置。query_result.go L160-L187
因此,下一个成功查询看到 blockIdx != 0 后跳过初始化,从 block 2 开始写,并为新的 statement ID 生成包含不存在的 block 1 的 metadata。query_result.go L63-L76 result_scan/meta_scan 随后可能读到错误路径、旧计数和旧过期时间。这正好破坏 issue #13060 的核心使用场景。
建议为 query-result generation 增加明确的 abort/reset 操作,并在 runner、saver、cancel、kill 的所有失败出口调用;长期应把这些 session 字段封装成每语句独立 generation。

[P2] 括号查询绕过 PERFORM ... INTO OUTFILE 拒绝
select_stmt 遇到括号查询时会新建一层 tree.Select,mysql_sql.y L6292-L6297,而 PERFORM 只给这层外部节点设置 IsPerform。mysql_sql.y L14442-L14446
Plan 校验只检查外层 stmt.Ep。build.go L436-L443 所以:
PERFORM (SELECT 1 INTO OUTFILE 'result.csv');
内层节点持有 Ep、外层 Ep == nil,不会命中显式拒绝,也不会初始化 frontend export config,最后会执行查询并返回 OK,而不是报 not supported。mysql_cmd_executor.go L4048-L4063
同一语法还通过 select_with_parens 接受 PERFORM (VALUES ROW(1)) 和 PERFORM (TABLE t),mysql_sql.y L6614-L6630,与 PR 声明的“non-SELECT statements 为 non-goal”不一致。
建议增加递归 AST 校验,或为 v1 使用只接受目标 SELECT 形态的专用 grammar production。

[P2] SIDECAR 路径完全绕过 PERFORM 的响应契约
COM_QUERY 在正常解析前拦截 /*+ SIDECAR */。mysql_cmd_executor.go L4938-L4960 Sidecar handler 会接受带 IsPerform 的 tree.Select,随后格式化 AST;formatter 又会把 perform 前缀写回 SQL。sidecar_offload.go L242-L279select.go L42-L45
结果是配置 sidecar 后,/
+ SIDECAR */ PERFORM SELECT ... 要么被下游 SQL 方言拒绝,要么成功后仍由 ExecRequest 返回 ResultResponse 和结果行,而不是 affected-rows=0 的单个 OK。
建议检测 sel.IsPerform 后回退到 CN pipeline;若确实需要 offload,则发送内部 SELECT,但在 MO 侧继续执行保存结果、抑制行和返回 OK 的完整契约。

[P2] 新的 "Perform" statement type 漏掉 trace 聚合
PR 将 statement type 从 "Select" 改成 "Perform"。select.go L77-L83 但 StatementInfoFilter 只聚合 "Select" 等旧类型,没有 "Perform"。report_statement.go L163-L175
因此符合聚合条件的短 PERFORM 会逐条写入原始 statement telemetry,而同等 SELECT 会合并;高频调用会显著增加 trace/ETL 存储量。建议加入 "Perform",或基于稳定的 DQL 分类判断,并补充过滤器用例。

@VioletQwQ-0
VioletQwQ-0 force-pushed the codex/issue-13060-perform branch from 4ee6b0a to 5964bac Compare August 3, 2026 11:37
@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed all four findings on exact head 5964bac357cee7fbf9a591e139dea77afc194ff4:

  • reset saved-query result generation state on PERFORM runner, cancellation, and finalization failures;
  • restrict PERFORM grammar to supported direct SELECT/CTE/UNION forms, rejecting parenthesized SELECT/VALUES/TABLE bypasses while retaining the planner rejection for direct SELECT INTO OUTFILE;
  • route SIDECAR/SIDECAR GPU PERFORM statements through the normal CN status-response path;
  • include Perform in motrace statement aggregation.

Validation: full pkg/frontend, pkg/sql/plan, pkg/sql/parsers/dialect/mysql, and pkg/util/trace/impl/motrace package tests passed with the MatrixOne CGo test wrapper; go vet and git diff --check passed; exact-head preflight is PASS review=PASS validation=PENDING. The prior coverage failure was an unrelated TestBasicCluster TN-readiness timeout on the superseded head; the push has started fresh exact-head CI.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex automated review

The four previously raised issues are addressed on the current head, but one blocking grammar/planner gap remains. Focused parser tests and diff validation passed; CGo-backed tests could not run locally because libmo.dylib is absent. The worktree is clean.

P2 - Reject INTO OUTFILE in parenthesized UNION branches (pkg/sql/plan/build.go:438)

This checks only the outer Select.Ep. The new perform_select delegates to simple_select, whose UNION productions accept parenthesized branches. Both PERFORM (SELECT 1 INTO OUTFILE 'result.csv') UNION SELECT 2 and the symmetric right-branch form parse with outer IsPerform=true/Ep=nil and inner Ep!=nil. getUnionSelects then passes that inner Select directly to bindSelect, and no recursive planner check reads its Ep, so the statement executes and returns OK instead of the promised not-supported error. Validate export parameters throughout the PERFORM query or restrict these grammar branches, with left/right UNION regression tests.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed the new exact-head finding by recursively rejecting INTO OUTFILE anywhere in the PERFORM query tree, including either side of a UNION. Added parser-to-planner regressions for top-level, left-branch, and right-branch exports. Focused and full owning-package tests, go vet, git diff --check, self-review, and exact-head preflight pass on 5782d90. GitHub CI is pending.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex automated review

Saved-result cleanup, unsupported parenthesized forms, SIDECAR handling, trace aggregation, and the prior left/right UNION blocker are fixed. However, the claimed rejection “anywhere” still misses CTE bodies and expression subqueries, leaving one blocking correctness gap. git diff --check passes; focused tests could not start because cgo/libmo.dylib is absent.

P2 - Reject INTO OUTFILE inside PERFORM CTEs and subqueries (pkg/sql/plan/build.go:435)

Author response: The author says this head recursively rejects INTO OUTFILE anywhere in the PERFORM query tree and added top-level and both UNION-side tests. Why this remains blocking: selectHasExportParam follows only Select.Select, ParenSelect, and UnionClause; it returns false at SelectClause and never inspects Select.With. The grammar stores a CTE body in With.CTEs[i].Stmt, so perform with c as (select 1 into outfile 'result.csv') select * from c has IsPerform=true and Ep=nil outside but an inner CTE Select with Ep!=nil. This check therefore passes, while the frontend also examines only the outer st.Ep, executes the query through the PERFORM status path, and returns OK instead of the promised not-supported error. This differs from the fixed UNION finding because the export is below With, not on the SelectStatement/UNION spine. Traverse CTE bodies and subqueries reachable from SelectClause, or enforce the exclusion in the grammar, with parser-to-planner regressions for those shapes.

@VioletQwQ-0

Copy link
Copy Markdown
Collaborator Author

Addressed the current-head review finding in 58b741a2aecd9cddce4a1c5faf54ad99aed56b3b.

The PERFORM guard now traverses the complete parser tree, so SELECT ... INTO OUTFILE nested in CTE bodies and expression subqueries is rejected before binding. Added parser-to-planner regressions for direct, CTE, projection-subquery, and predicate-subquery forms, plus nearest valid controls and production BVT cases.

Validation:

  • full pkg/sql/plan test package: PASS
  • git diff --check: PASS
  • exact-head preflight: PASS review=PASS validation=PENDING

GitHub CI for this exact head is now running.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex automated review

All prior blockers are resolved. Saved-result failure cleanup, parenthesized-form restrictions, SIDECAR routing, trace aggregation, UNION export rejection, and the CTE/expression-subquery export gap are fixed. The latest traversal reaches those nested AST forms and is covered by rejection and valid-control tests, so the previous blocker is withdrawn. No remaining finding identified; git diff --check passes. Local package execution was unavailable because cgo/libmo.dylib is absent.

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Codex automated review

Revalidated the complete change at the unchanged previously approved head; the follow-up delta is empty. Saved-result failure cleanup, parenthesized-form restrictions, SIDECAR routing, trace aggregation, UNION export rejection, and the CTE/expression-subquery export blocker all remain fixed. The latter blocker remains withdrawn. Focused parser tests and diff validation pass; no new finding identified.

@mergify

mergify Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-08-03 13:35 UTC · Rule: main · triggered by rule Automatic queue on approval for main
  • 🚫 Left the queue2026-08-03 14:23 UTC · at 58b741a2aecd9cddce4a1c5faf54ad99aed56b3b

This pull request spent 47 minutes 59 seconds in the queue, with no time running CI.

Reason

The pull request can't be updated

merge conflict between base and head

Hint

You should update or rebase your pull request manually. If you do, this pull request will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Requeued — the merge queue status continues in this comment ↓.

…erform

# Conflicts:
#	pkg/sql/parsers/dialect/mysql/mysql_sql.go
@mergify

mergify Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-08-03 15:29 UTC · Rule: main · triggered by rule Automatic queue on approval for main
  • 🟠 Checks running · in-place
  • 🚫 Left the queue2026-08-03 15:51 UTC · at 513d4510e3ad523d92205822e24c0ab6c78986f0

This pull request spent 22 minutes 22 seconds in the queue, with no time running CI.

Reason

The pull request can't be updated

This pull request seems to come from a fork, and Mergify needs the author's permission to update its branch.
The author needs to enable "Allow edits from maintainers" on this pull request, or update the branch manually.

Hint

You should update or rebase your pull request manually. If you do, this pull request will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Tick the box to put this pull request back in the merge queue (same as @mergifyio queue).

  • Requeue this pull request

@VioletQwQ-0 VioletQwQ-0 mentioned this pull request Aug 3, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dequeued kind/api-change kind/feature size/XXL Denotes a PR that changes 2000+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants