Files
doris/regression-test/plugins/plugin_planner.groovy
T
yujun fe39f5b6a4 [fix](ivm) Answer FE-computable dry runs on the frontend instead of a placeholder backend (#67753)
`REFRESH MATERIALIZED VIEW ... INCREMENTAL WITH DRY RUN` fails with
`Unable to resolve host dummy` in cloud mode when the delta is empty.

An empty delta plans to a `LogicalEmptyRelation`, so the Nereids planner
marks the plan as not needing a backend (`notNeedBackend`) and binds the
placeholder `Backend(-1, "dummy", -1)`. Regular queries short-circuit
such FE-computable plans inside `handleQueryInFe`; the internal
streaming entry used by IVM dry runs (`RefreshMTMVCommand.dryRunRefresh`
-> `StmtExecutor.executeInternalQueryAndSend`) skipped that
short-circuit and started a coordinator, which then sent fragments to
`dummy:-1`.

### What changed

- `StmtExecutor.executeInternalQueryCommon` now short-circuits
FE-computable plans through `planner.handleQueryInFe` for both the
streaming (`sendChannel`) and the collecting internal entry, writing
rows to the caller's mysql channel.
- `sendResultSet`, `sendMetaData`, `sendTextResultRow` and
`sendBinaryResultRow` take an explicit `MysqlChannel` overload; the
previous signatures keep using the executor's own channel.
- `isHandleQueryInFe` and `ExecutedByFrontend` are set only after the
result set was sent, matching the regular query path.
- `test_ivm_refresh_dry_run` no longer skips cloud. The new
`ivm_dry_run_qt` helper masks `__DORIS_SEQUENCE_COL__` (whose base value
differs between cloud and shared-nothing) through the `quickRunTest` row
converter, which now optionally receives the result metadata, so both
modes share one suite and one `.out`. Both suites cover the empty-delta
dry run.

Trace issue: https://github.com/apache/doris/issues/65418
2026-09-11 17:21:05 +08:00

159 lines
5.3 KiB
Groovy

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import org.apache.doris.regression.suite.Suite
def anlyzedPlanShowFields = [
LogicalProject : ['projects'],
LogicalSort : ['orderKeys'],
LogicalFilter : ['predicates'],
LogicalHaving : ['predicates'],
LogicalQualify : ['predicates'],
LogicalAggregate : ['groupByExpr', 'outputExpr'],
LogicalOlapScan : ['qualified'],
LogicalResultSink : ['outputExprs'],
LogicalJoin : ['type', 'markJoinSlotReference', 'hashJoinConjuncts', 'otherJoinConjuncts', 'markJoinConjuncts'],
]
def extractAttributes(String input) {
// 正则解释:
// (\w+) - 匹配key
// = - 等号
// (.+?) - 非贪婪匹配value(任意字符)
// (?= - 正向向前查找
// \s*,\s*\w+= - 后面是逗号+空格+下一个key=
// | - 或
// \s*\) - 后面是空格+右括号
// | - 或
// $ - 字符串结束
// )
// def pattern = /(\w+)=(.+?)(?=\s*,\s*\w+=|\s*\)|$)/
def pattern = /(\w+)=(.+?)(?=\s*,\s*\w+=|\s\)|$)/
def matcher = input =~ pattern
def result = [:]
matcher.each { match ->
result[match[1]] = match[2].trim()
}
return result
}
def convertPlanRow = { row, showFieldMap ->
def cell = row[0]
def s = ""
for (def line : cell.split("\n")) {
if (!s.isEmpty()) {
s += "\n"
}
def p1 = line.findIndexOf { Character.isLetter(it as char) }
if (p1 == -1) {
s += line
continue
}
def p2 = p1 + line.substring(p1).findIndexOf { !Character.isLetter(it as char) }
if (p2 < p1) {
s += line
continue
}
def planName = line.substring(p1, p2)
def showFields = showFieldMap.getOrDefault(planName, [])
s += line.substring(0, p2)
s += " ( "
def showAttrs = []
def attrs = extractAttributes(line)
for (def field : showFields) {
if (attrs.containsKey(field)) {
showAttrs.add("${field}=${attrs[field]}".toString())
}
}
s += showAttrs.join(", ")
s += " )"
}
return [s]
}
def convertIvmExplainRow = { row ->
def planIndex = row.size() - 1
def plan = row[planIndex].toString()
.replaceAll(/#\d+/, "#")
.replaceAll(/\b([A-Za-z][A-Za-z0-9_]*)\[\d+\]/, '$1[]')
.replaceAll(/selectedIndexId=[^,\s\)]+/, "selectedIndexId=<id>")
def converted = []
for (int i = 0; i < row.size(); i++) {
converted.add(i == planIndex ? plan : row[i].toString())
}
return converted
}
Suite.metaClass.explainAndResult = { String tag, String sql ->
"qt_${tag}_shape" "explain shape plan ${sql}"
"qt_${tag}_result" "${sql}"
}
Suite.metaClass.explainAndOrderResult = { String tag, String sql ->
"qt_${tag}_shape" "explain shape plan ${sql}"
"order_qt_${tag}_result" "${sql}"
}
Suite.metaClass.explainAnalyzedPlan = { String tag, String sql, overwriteShowFields = [:] ->
def showFields = anlyzedPlanShowFields
if (!overwriteShowFields.isEmpty()) {
showFields = [:]
showFields.putAll(anlyzedPlanShowFields)
showFields.putAll(overwriteShowFields)
}
delegate.quickRunTest(
tag,
"explain analyzed plan ${sql}".toString(),
false,
{row -> convertPlanRow(row, showFields) }
)
}
Suite.metaClass.explainIvmPlan = { String tag, String sql ->
delegate.quickRunTest(
tag,
sql,
false,
convertIvmExplainRow
)
}
// __DORIS_SEQUENCE_COL__ of an IVM dry-run delta encodes the refresh version, whose base value
// differs between cloud and shared-nothing deployments. Mask that column so that both modes can
// share one suite and one .out file.
def maskIvmDryRunSequence = { row, meta ->
for (int i = 1; i <= meta.getColumnCount(); i++) {
if ("__DORIS_SEQUENCE_COL__".equalsIgnoreCase(meta.getColumnLabel(i))) {
def masked = new ArrayList(row)
masked.set(i - 1, "[regression-fake-sequence]")
return masked
}
}
return row
}
// Named ivm_dry_run_qt instead of order_qt_*: Suite.invokeMethod intercepts every method whose
// name starts with qt_ / order_qt_ and derives the tag from the method name, so a metaClass
// method could never be reached under those prefixes.
Suite.metaClass.ivm_dry_run_qt = { String tag, String sql ->
delegate.quickRunTest(tag, sql, true, maskIvmDryRunSequence)
}