github-actions[bot] commented on code in PR #65984:
URL: https://github.com/apache/doris/pull/65984#discussion_r3649596579
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalTable.java:
##########
@@ -106,6 +106,22 @@ public Table getPaimonTable(Optional<MvccSnapshot>
snapshot) {
}
}
+ public Table getPaimonTable(TableScanParams scanParams) {
+ if (scanParams != null && scanParams.isOptions()) {
+ // Statement MVCC is keyed by table, while OPTIONS belongs to one
relation. Start
+ // from the catalog handle so its schema cannot inherit another
relation's selector.
+ return PaimonScanParams.applyOptions(getBasePaimonTable(),
scanParams.getMapParams());
Review Comment:
[P1] Preserve statement MVCC for selector-free OPTIONS
`BindRelation` has already stored one exact Paimon snapshot per table, but
this branch discards it even when the option is not a snapshot selector. If
snapshot S is captured and S+1 commits before scan initialization, a plain
alias keeps S through `PaimonSource`, while an alias with
`@options('scan.plan-sort-partition'='true')` calls `newScan()` on the unpinned
base handle and reads S+1. It can also inherit a catalog/table-default selector
because `applyOptions` clears selectors only when the request supplies one. A
behavioral query option can therefore mix snapshots inside one statement.
Please overlay selector-free options on the table stored in statement MVCC,
reserve a cleaned base handle for explicit relation-local selectors, and cover
a two-alias commit/default-selector case.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindRelation.java:
##########
@@ -725,6 +732,8 @@ private LogicalPlan getLogicalPlan(TableIf table,
UnboundRelation unboundRelatio
Utils.qualifiedNameWithBackquote(qualifiedTableName));
});
+ validateOptionsTarget(table, unboundRelation.getScanParams());
Review Comment:
[P2] Validate OPTIONS before every relation or command can discard it
This check runs only after a catalog-backed query relation is resolved. A
CTE returns `LogicalCTEConsumer` before reaching it, and a recursive CTE
similarly returns `LogicalWorkTableReference`, so `WITH c AS (...) SELECT *
FROM c@options('scan.snapshot-id'='1')` silently ignores the request. The
shared `baseTableRef` grammar also feeds commands such as SHOW REPLICA into
`TableRefInfo`; those consumers never read `scanParams`, so they silently
ignore OPTIONS too. Please reject OPTIONS at each early return and non-query
consumer, or move capability validation to a boundary shared by every
syntactically accepted `baseTableRef`, with CTE and representative command
negative tests.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonScanParams.java:
##########
@@ -0,0 +1,113 @@
+// 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.
+
+package org.apache.doris.datasource.paimon;
+
+import org.apache.doris.analysis.TableScanParams;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.table.Table;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Validation and application rules for relation-scoped Paimon scan parameters.
+ */
+public final class PaimonScanParams {
+ private static final Set<String> QUERY_OPTION_KEYS =
CoreOptions.getOptions().stream()
+ .map(option -> option.key())
+ .filter(key -> key.startsWith("scan."))
+ .collect(Collectors.toSet());
+
+ private static final Set<String> SNAPSHOT_SELECTOR_KEYS = ImmutableSet.of(
+ CoreOptions.SCAN_SNAPSHOT_ID.key(),
+ CoreOptions.SCAN_TAG_NAME.key(),
+ CoreOptions.SCAN_TIMESTAMP.key(),
+ CoreOptions.SCAN_TIMESTAMP_MILLIS.key(),
+ CoreOptions.SCAN_WATERMARK.key(),
+ CoreOptions.SCAN_VERSION.key());
+
+ private static final Set<String> INCREMENTAL_SYSTEM_TABLES =
ImmutableSet.of(
+ "audit_log", "binlog", "row_tracking");
+
+ private static final Set<String> OPTIONS_SYSTEM_TABLES = ImmutableSet.of(
+ "audit_log", "binlog", "files", "manifests", "partitions", "ro",
"row_tracking");
+
+ private PaimonScanParams() {
+ }
+
+ public static void validateOptions(Map<String, String> options) {
+ Set<String> unsupported = options.keySet().stream()
+ .filter(key -> !QUERY_OPTION_KEYS.contains(key))
+ .collect(Collectors.toSet());
+ if (!unsupported.isEmpty()) {
+ throw new IllegalArgumentException("Unsupported Paimon query
option(s): " + unsupported);
+ }
+
+ long selectorCount =
options.keySet().stream().filter(SNAPSHOT_SELECTOR_KEYS::contains).count();
+ if (selectorCount > 1) {
+ throw new IllegalArgumentException(
+ "Only one Paimon snapshot selector can be specified: " +
SNAPSHOT_SELECTOR_KEYS);
+ }
+ }
+
+ public static Table applyOptions(Table table, Map<String, String> options)
{
+ validateOptions(options);
+ Map<String, String> isolatedOptions = new HashMap<>(options);
+ if
(options.keySet().stream().anyMatch(SNAPSHOT_SELECTOR_KEYS::contains)) {
Review Comment:
[P2] Isolate scan mode and position in both directions
This removes competing selector keys, but it leaves a table/catalog-default
`scan.mode`. With default `scan.mode=latest`,
`@options('scan.snapshot-id'='1')` therefore fails Paimon validation under
`LATEST`. The reverse is broken too: if the table carries `scan.snapshot-id=7`,
local `@options('scan.mode'='latest')` has no selector key, so cleanup never
runs and the retained snapshot is rejected under `LATEST`. Please isolate mode
and startup position as one compatibility-aware state: clear incompatible
inherited counterparts, while still rejecting explicitly conflicting
query-local pairs, and add regressions for both directions.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonExternalTable.java:
##########
@@ -106,6 +106,22 @@ public Table getPaimonTable(Optional<MvccSnapshot>
snapshot) {
}
}
+ public Table getPaimonTable(TableScanParams scanParams) {
+ if (scanParams != null && scanParams.isOptions()) {
+ // Statement MVCC is keyed by table, while OPTIONS belongs to one
relation. Start
+ // from the catalog handle so its schema cannot inherit another
relation's selector.
+ return PaimonScanParams.applyOptions(getBasePaimonTable(),
scanParams.getMapParams());
+ }
+ return getPaimonTable(MvccUtil.getSnapshotFromContext(this));
+ }
+
+ public List<Column> getFullSchema(TableScanParams scanParams) {
+ Table table = getPaimonTable(scanParams);
+ return PaimonUtil.parseSchema(table,
Review Comment:
[P1] Preserve nested Paimon field IDs in the OPTIONS schema
`parseSchema` assigns `field.id()` only to each top-level `Column`; all
STRUCT, MAP, and ARRAY descendants created by `Column.createChildrenColumn`
keep the default ID `-1`. The normal Paimon schema path calls
`updatePaimonColumnUniqueId` recursively. Here `ExternalUtil` serializes the
missing nested IDs, `AccessPathParser` substitutes zero-based child ordinals,
and the native reader treats those fake ordinals as real IDs under
`BY_FIELD_ID`. A nested child renamed, reordered, or dropped and re-added is
therefore materialized as missing/default, or can match the wrong sibling on an
ID collision. Please build this schema through the schema cache or recursively
populate nested IDs and TIMESTAMPTZ metadata, then cover OPTIONS reads across
nested evolution.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonScanParams.java:
##########
@@ -0,0 +1,113 @@
+// 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.
+
+package org.apache.doris.datasource.paimon;
+
+import org.apache.doris.analysis.TableScanParams;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.table.Table;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Validation and application rules for relation-scoped Paimon scan parameters.
+ */
+public final class PaimonScanParams {
+ private static final Set<String> QUERY_OPTION_KEYS =
CoreOptions.getOptions().stream()
+ .map(option -> option.key())
+ .filter(key -> key.startsWith("scan."))
Review Comment:
[P1] Do not accept fallback-branch through plain Table.copy
This prefix allowlist admits Paimon 1.3.1's `scan.fallback-branch`, but
`applyOptions` implements every accepted key with `table.copy(...)`. Paimon
creates `FallbackReadFileStoreTable` only in `FileStoreTableFactory.create`,
where it resolves the named branch. Copying a plain table merely stores the
option and remains a plain append-only or primary-key table; copying an
existing fallback wrapper keeps its already-resolved fallback object even if
the query names another branch. Thus
`@options('scan.fallback-branch'='archive')` can succeed while silently
omitting partitions that exist only in `archive`, or can read a previously
configured fallback branch. Please route this option through factory/catalog
resolution, or reject it, and add a two-branch result test that switches the
requested fallback.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -924,8 +951,17 @@ private Table getProcessedTable() throws UserException {
}
if (theScanParams != null && theScanParams.incrementalRead()) {
+ // System table handles are cached, so preserve query isolation by
applying dynamic
+ // options to a copied Paimon table instead of changing the shared
handle.
return baseTable.copy(getIncrReadParams());
Review Comment:
[P2] Clear all inherited startup state for system-table INCR
`getIncrReadParams()` removes only `scan.snapshot-id` and `scan.mode`, so
the newly supported wrappers retain other positions and range controls. With
default `scan.tag-name=tag1`, `$audit_log@incr(1,2)` is rejected under
incremental mode. An inherited watermark is silently worse: Paimon time-travels
the wrapped table to the watermark-era schema before validation, does not list
watermark as an INCREMENTAL conflict, then scans `(1,2)` through that old
schema, which can disagree with Doris's current bound columns after evolution.
Please clear every inherited startup position and alternate range control
through one isolation helper, and cover both a rejected tag and an
evolved-schema watermark case.
##########
fe/fe-core/src/main/java/org/apache/doris/analysis/TableScanParams.java:
##########
@@ -29,12 +30,14 @@ public class TableScanParams {
public static final String INCREMENTAL_READ = "incr";
public static final String BRANCH = "branch";
public static final String TAG = "tag";
+ public static final String OPTIONS = "options";
public static final String SNAPSHOT = "snapshot";
public static final String RESET = "reset";
private static final ImmutableSet<String> VALID_PARAM_TYPES =
ImmutableSet.of(
INCREMENTAL_READ,
BRANCH,
- TAG);
+ TAG,
+ OPTIONS);
Review Comment:
[P1] Preserve OPTIONS through CREATE VIEW's relation copy
Adding OPTIONS to global relation parameters also makes it valid in a view
query, but `LogicalPlanBuilderForCreateView` immediately calls
`UnboundRelation.withIndexInSql`, whose copy passes `null` for `scanParams`.
Creation-time analysis and `finalCols` therefore use the ordinary/latest table.
The SQL rewrite replaces only the table identifier, so the persisted definition
still contains `@options(...)`; later expansion parses it normally and binds
the selected snapshot schema. After evolution, `CREATE VIEW v AS SELECT * FROM
p@options('scan.snapshot-id'='1')` is declared from latest columns but expands
with historical slots, breaking the view contract. Please preserve `scanParams`
in every identity copy, or reject scan params in persisted views, and add an
evolved-schema CREATE/ALTER VIEW regression.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonScanParams.java:
##########
@@ -0,0 +1,113 @@
+// 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.
+
+package org.apache.doris.datasource.paimon;
+
+import org.apache.doris.analysis.TableScanParams;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.table.Table;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Validation and application rules for relation-scoped Paimon scan parameters.
+ */
+public final class PaimonScanParams {
+ private static final Set<String> QUERY_OPTION_KEYS =
CoreOptions.getOptions().stream()
+ .map(option -> option.key())
+ .filter(key -> key.startsWith("scan."))
+ .collect(Collectors.toSet());
+
+ private static final Set<String> SNAPSHOT_SELECTOR_KEYS = ImmutableSet.of(
Review Comment:
[P1] Isolate creation-time startup positions too
`QUERY_OPTION_KEYS` accepts `scan.creation-time-millis` and
`scan.file-creation-time-millis`, but this selector set omits both, so they
neither participate in explicit-conflict counting nor clear inherited
positions. For a table carrying default `scan.snapshot-id=7`,
`@options('scan.creation-time-millis'='T')` retains snapshot 7. Paimon 1.3.1
derives `scan.mode=from-snapshot`, selects snapshot 7, and does not validate
creation time as a conflict, so the query succeeds while silently ignoring T.
The file-creation variant fails conflict validation instead. Please include
every mutually exclusive startup-position key in one isolation/validation
model, clear inherited mode and positions when one is supplied, and add a
result test that distinguishes the default snapshot from the requested creation
time.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]