github-actions[bot] commented on code in PR #65984:
URL: https://github.com/apache/doris/pull/65984#discussion_r3649196510


##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -924,8 +930,14 @@ 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());
         }
+        if (theScanParams != null && theScanParams.isOptions()) {
+            // Apply per-query options to a copy so they cannot leak through 
cached table handles.
+            return baseTable.copy(theScanParams.getMapParams());

Review Comment:
   [P1] Bind the ordinary-table schema selected by OPTIONS
   
   `PaimonExternalTable.loadSnapshot` treats an OPTIONS relation as the latest 
snapshot, so analysis, tuple slots, and predicates are bound to the latest 
schema before this copy runs. For `scan.snapshot-id`, Paimon then swaps the 
copied table to that snapshot's historical schema: FE filters latest-only slots 
out of its projection, while JNI still requests them and fails (`RequiredField 
... not found`); native/C++ readers keep the same 
latest-descriptor/historical-split mismatch. The existing schema thread covers 
system-table wrappers; this is the separate ordinary-table MVCC path. Please 
resolve schema-selecting OPTIONS before relation binding and add an 
evolved-schema data-table regression.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -924,8 +930,14 @@ 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());
         }
+        if (theScanParams != null && theScanParams.isOptions()) {
+            // Apply per-query options to a copy so they cannot leak through 
cached table handles.
+            return baseTable.copy(theScanParams.getMapParams());

Review Comment:
   [P1] Resolve alternate snapshot selectors before this copy
   
   The ordinary-table handle reaching this branch is already pinned by 
`PaimonLatestSnapshotProjectionLoader` with `scan.snapshot-id=<latest>`. 
Replacing that same key works, but valid Paimon selectors such as 
`scan.tag-name`, `scan.timestamp`, `scan.timestamp-millis`, or `scan.watermark` 
are merged beside the hidden snapshot ID; Paimon 1.3.1 requires exactly one 
selector and `newScan()` fails with its one-parameter error. Please resolve 
OPTIONS while building the per-relation Paimon snapshot (or remove the mutually 
exclusive cached selector before copying), and cover at least one non-ID 
selector on a real MVCC-backed table.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -924,8 +930,14 @@ 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());
         }
+        if (theScanParams != null && theScanParams.isOptions()) {
+            // Apply per-query options to a copy so they cannot leak through 
cached table handles.
+            return baseTable.copy(theScanParams.getMapParams());

Review Comment:
   [P2] Validate option names before relying on Table.copy
   
   Paimon 1.3.1 is not the key-validation boundary assumed here: 
`AbstractFileStoreTable.copy` merges arbitrary keys, `checkAlterTableOption` 
only rejects immutable/bucket changes, and `SchemaValidation` explicitly does 
not validate every option name. Consequently `@options('scan.snapsh0t-id'='1')` 
succeeds, the typo is never consumed, and Doris returns the latest/default rows 
instead of the requested historical data. Please validate the supported 
query-option key space (including any intentional extension namespaces) and add 
a negative typo case.



##########
regression-test/suites/external_table_p0/paimon/paimon_incr_read.groovy:
##########
@@ -43,6 +43,55 @@ suite("test_paimon_incr_read", "p0,external") {
 
         def test_incr_read = { String force ->
             sql """ set force_jni_scanner=${force} """
+
+            // Query-level OPTIONS params apply to ordinary data tables for 
both native and JNI readers.
+            List<List<Object>> latestRows = sql """
+                    select id, name, age from paimon_incr order by id
+                    """
+            List<List<Object>> snapshot1Rows = sql """
+                    select id, name, age from paimon_incr
+                    @options('scan.snapshot-id'='1')
+                    order by id
+                    """
+            List<List<Object>> snapshot2Rows = sql """
+                    select id, name, age from paimon_incr
+                    @options('scan.snapshot-id'='2')
+                    order by id
+                    """
+            List<List<Object>> latestSnapshotRows = sql """
+                    select id, name, age from paimon_incr
+                    @options('scan.snapshot-id'='3')
+                    order by id
+                    """
+            assertEquals(1, snapshot1Rows.size())
+            assertEquals(2, snapshot2Rows.size())
+            assertEquals(3, latestRows.size())
+            assertEquals(latestRows, latestSnapshotRows)
+
+            // A dynamic option must not leak into the cached table used by 
the next query.
+            assertEquals(latestRows, sql("select id, name, age from 
paimon_incr order by id"))
+
+            List<List<Object>> relationScopedRows = sql """
+                    select left_orders.id, right_orders.id
+                    from paimon_incr
+                    @options('scan.snapshot-id'='1') left_orders
+                    join paimon_incr
+                    @options('scan.snapshot-id'='2') right_orders
+                    on left_orders.id = right_orders.id
+                    order by left_orders.id
+                    """
+            assertEquals(1, relationScopedRows.size())

Review Comment:
   [P2] Make the right-hand snapshot affect this oracle
   
   Snapshot 1 contains only ID 1, while snapshot 2 and the latest snapshot also 
contain ID 1. Because the left input is fixed at snapshot 1 and this is an 
inner equality join, the result is still exactly `(1,1)` if the right-hand 
option is lost, replaced with snapshot 1, or executed at latest, so all three 
assertions pass without proving relation-scoped execution. Please add a result 
whose value depends on ID 2 being present only on the right (for example, drive 
an anti/outer check from the snapshot-2 relation), and record it with a 
generated `.out`, so dropping the second option fails the regression.



##########
fe/fe-core/src/main/java/org/apache/doris/analysis/TableScanParams.java:
##########
@@ -29,12 +29,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:
   [P2] Reject OPTIONS on unsupported table types
   
   Adding `OPTIONS` to this global set makes the generic relation grammar 
accept it for every catalog, but only `PaimonScanNode` consumes `isOptions()`. 
For example, a Hive relation is carried through `LogicalFileScan` and 
`PhysicalPlanTranslator`, then `HiveScanNode` never reads the field, so `SELECT 
* FROM hive_catalog.db.t@options('scan.snapshot-id'='1')` succeeds against the 
normal table and silently discards the request; several other file-scan types 
behave the same way. Please validate OPTIONS against the bound table type (and 
add representative negative tests) so unsupported catalogs fail instead of 
returning misleading data.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -924,8 +930,14 @@ 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());
         }
+        if (theScanParams != null && theScanParams.isOptions()) {
+            // Apply per-query options to a copy so they cannot leak through 
cached table handles.
+            return baseTable.copy(theScanParams.getMapParams());

Review Comment:
   [P2] Reject OPTIONS forms that this branch cannot consume
   
   The generic scan-parameter grammar also accepts `@options()` and positional 
identifier forms such as `@options(foo, bar)`. Those arguments are stored in 
`listParams`, while this branch forwards only the empty `mapParams`, so the 
query succeeds as a latest/default scan instead of reporting malformed syntax. 
Please require OPTIONS to be a non-empty key/value map and add negative 
parser/analyzer coverage for both empty and positional forms.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/source/PaimonScanNode.java:
##########
@@ -912,8 +918,8 @@ private Table getProcessedTable() throws UserException {
         Table baseTable = source.getPaimonTable();
         TableScanParams theScanParams = getScanParams();
         if (source.getExternalTable() instanceof PaimonSysExternalTable) {

Review Comment:
   [P1] Keep row_tracking incremental reads on the Paimon reader
   
   This broadened acceptance also enables `t$row_tracking@incr(...)`. For an 
append-only row-tracking table, its `DataSplit` is raw-convertible, and 
`shouldForceJniForSystemTable()` forces only audit/binlog, so 
`force_jni_scanner=false` sends it through `PAIMON_NATIVE`. Paimon's 
row-tracking reader fills lazily stored `_ROW_ID` and `_SEQUENCE_NUMBER` from 
`DataFileMeta.firstRowId/maxSequenceNumber`, but Doris's raw-file split carries 
neither value and has no equivalent row-tracking special-field mapping. 
First-write rows can therefore return missing/wrong hidden values. Please force 
this system table to JNI (or carry and materialize the metadata) and add a 
native-mode first-snapshot regression.



##########
regression-test/suites/external_table_p0/paimon/paimon_system_table.groovy:
##########
@@ -151,7 +151,56 @@ suite("paimon_system_table", "p0,external") {
                         desc ${tableName}\$snapshots
                         """
 
-        // 2.6 system table does not support time travel
+        // 2.6 system table supports dynamic scan options but not time travel
+        // Quote Paimon's partition column because PARTITION is reserved in 
Doris SQL.
+        List<List<Object>> filesAtSnapshotResult = sql """
+                SELECT `partition`,
+                       bucket,
+                       file_path,
+                       file_format,
+                       schema_id,
+                       level,
+                       record_count,
+                       file_size_in_bytes,
+                       min_sequence_number,
+                       max_sequence_number,
+                       creation_time
+                FROM ${tableName}\$files
+                @options('scan.snapshot-id'='${direct_query_snapshot_id}')
+                """
+        assertTrue(filesAtSnapshotResult.size() > 0,
+                "Files system table should return data for snapshot 
${direct_query_snapshot_id}")
+
+        String multiSnapshotTable = "test_paimon_incr_read_db.paimon_incr"
+        List<List<Object>> snapshotRows = sql """
+                select snapshot_id from ${multiSnapshotTable}\$snapshots order 
by snapshot_id
+                """
+        assertTrue(snapshotRows.size() > 1, "The regression table should 
contain multiple snapshots")
+        String firstSnapshotId = String.valueOf(snapshotRows.first()[0])
+        String latestSnapshotId = String.valueOf(snapshotRows.last()[0])
+        List<List<Object>> filesWithoutOptions = sql """
+                select file_path from ${multiSnapshotTable}\$files order by 
file_path
+                """
+        List<List<Object>> filesAtFirstSnapshot = sql """
+                select file_path from ${multiSnapshotTable}\$files
+                @options('scan.snapshot-id'='${firstSnapshotId}')
+                order by file_path
+                """
+        List<List<Object>> filesAtLatestSnapshot = sql """
+                select file_path from ${multiSnapshotTable}\$files
+                @options('scan.snapshot-id'='${latestSnapshotId}')
+                order by file_path
+                """
+        assertTrue(filesAtFirstSnapshot.size() < filesWithoutOptions.size(),

Review Comment:
   [P2] Assert the requested historical snapshot exactly
   
   The fixture has one, two, and three files at snapshots 1, 2, and 3, but this 
condition accepts either of the first two: resolving the requested first 
snapshot as snapshot 2 still satisfies `2 < 3`. The later assertion verifies 
only the latest selector, so this regression can pass while the historical 
option selects the wrong snapshot. Please assert the known count/file set for 
snapshot 1 and record the deterministic result through the regression `.out` 
path.



-- 
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]

Reply via email to