This is an automated email from the ASF dual-hosted git repository.

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 77626ad5f1d [fix](paimon) Bind REST schemas to stored table 
generations (#69158)
77626ad5f1d is described below

commit 77626ad5f1d48d92a9a1be3ee1790310a0ec803f
Author: Gabriel <[email protected]>
AuthorDate: Fri Oct 16 15:42:26 2026 +0800

    [fix](paimon) Bind REST schemas to stored table generations (#69158)
    
    ### What problem does this PR solve?
    
    A normal SELECT through a Paimon REST catalog can fail with:
    
    ```
    Paimon table generation changed after the statement was pinned; retry the 
statement
    ```
    
    For example, `test_paimon_rest_catalog_write` creates a table and
    completes its INSERT statements, then fails while planning the first
    SELECT. RESTCatalog reconstructs `TableSchema` from a REST `Schema`
    using `TableSchema.create`. Its creation time, schema-format version and
    highest allocated field ID need not match the physical schema. Comparing
    these synthetic values to the stored schema falsely reports replacement.
    
    Bind REST schemas to the physical schema at the same ID before caching.
    To keep metadata and physical identity consistent during DROP/CREATE,
    use the first REST response only to locate schema files, capture their
    full identities, reload REST metadata without the Doris table cache, and
    require the physical identities to remain unchanged. The comparison
    includes stored options, branch/path and fallback shape. Then verify the
    logical fields/keys/comment and retain the fresh REST options, FileIO
    and catalog callbacks.
    
    For example, replacing a primary-key table `(x, y)` with `bucket=2,
    bucket-key=x` by one using `bucket-key=y` must never combine the
    replacement schema identity with the original options: `WHERE x=1` could
    otherwise prune buckets containing matching rows. A replacement before
    the physical-read bracket uses the fresh response; a replacement during
    it fails the load without caching it. Fallback leaves are bound and
    checked independently.
    
    Cache hits never rebind retained handles. Existing strict
    schema/snapshot generation checks remain unchanged. Each REST table
    cache miss now performs two REST table lookups and two physical schema
    reads per leaf; cache hits add no I/O. Non-REST catalogs and non-file
    tables retain their existing loading path.
    
    ### Validation
    
    - Nine REST schema tests cover synthetic metadata, cache
    enabled/disabled, null/same UUID, option/IO/environment preservation,
    replacements before/during binding (including equal creation
    timestamps), retry after failed loading, retained-handle rejection,
    refresh, logical mismatch, changed location and fallback
    replacement/shape changes.
    - A real local Paimon write/read fixture reproduces missing rows with
    mixed-generation bucket options and reads all 16 matching rows after the
    fix. The original implementation failed the three initial race tests.
    - `mvn -f fe/pom.xml -pl :fe-connector-paimon -am install
    -Dmaven.build.cache.enabled=false` passed, including FE Checkstyle and
    plugin zip assembly: 1,465 tests, zero failures/errors, one existing
    live-connectivity test skipped. The Paimon module has 811 passing tests
    and that one skip.
    - The REST write regression also checks refresh/re-read consistency
    after its existing golden assertion. Groovy syntax compilation and `git
    diff --check` passed.
    
    Tests use real local Paimon files and SDK readers/writers with a
    recording REST catalog. A live REST/DLF service was not exercised
    locally; the external regression remains for CI. The existing live test
    skips when `PAIMON_WAREHOUSE` is not configured.
    
    ### Release note
    
    Fix false table-generation-change errors and prevent mixed-generation
    metadata when reading Paimon REST tables.
    
    ### Check List (For Author)
    
    - [x] Unit Test
    - [x] Regression test extension
    - [x] Behavior changed: valid REST schemas are accepted; concurrent
    replacement during binding is rejected
    - [x] No documentation needed
---
 .../connector/paimon/PaimonMetaCacheCatalog.java   |  11 +-
 .../doris/connector/paimon/PaimonSchemaPin.java    |  64 ++++
 .../connector/paimon/PaimonRestSchemaTest.java     | 347 +++++++++++++++++++++
 .../paimon/test_paimon_rest_catalog_write.groovy   |   5 +
 4 files changed, 425 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalog.java
 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalog.java
index b537b6f1e13..d82e67ec163 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalog.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonMetaCacheCatalog.java
@@ -45,6 +45,7 @@ import org.apache.paimon.utils.SegmentsCache;
 
 import java.time.Duration;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BiConsumer;
@@ -211,8 +212,14 @@ final class PaimonMetaCacheCatalog extends DelegateCatalog 
{
                 try {
                     cached = tableCache.get(identifier, ignored -> {
                         try {
-                            Table loaded = 
attachPerTableCaches(super.getTable(identifier));
-                            return new ExpiringValue<>(loaded, 
nanoTime.getAsLong());
+                            Table loaded = super.getTable(identifier);
+                            if (loaded instanceof FileStoreTable
+                                    && 
PaimonCatalogOps.CatalogBackedPaimonCatalogOps.restCatalog(wrapped) != null) {
+                                Map<String, String> schemas =
+                                        
PaimonSchemaPin.captureRestSchemas((FileStoreTable) loaded);
+                                loaded = 
PaimonSchemaPin.bindRestSchema(super.getTable(identifier), schemas);
+                            }
+                            return new 
ExpiringValue<>(attachPerTableCaches(loaded), nanoTime.getAsLong());
                         } catch (TableNotExistException e) {
                             throw new CatalogLoadException(e);
                         }
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaPin.java
 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaPin.java
index 7cca13244be..0b633f4c452 100644
--- 
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaPin.java
+++ 
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonSchemaPin.java
@@ -21,10 +21,12 @@ import 
org.apache.doris.connector.spi.DorisConnectorException;
 
 import com.google.common.hash.Hashing;
 import org.apache.paimon.Snapshot;
+import org.apache.paimon.schema.SchemaManager;
 import org.apache.paimon.schema.TableSchema;
 import org.apache.paimon.table.DataTable;
 import org.apache.paimon.table.DelegatedFileStoreTable;
 import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.utils.SnapshotManager;
 
@@ -125,6 +127,68 @@ final class PaimonSchemaPin {
         }
     }
 
+    static Map<String, String> captureRestSchemas(FileStoreTable table) {
+        Map<String, String> schemas = new HashMap<>();
+        captureRestSchemas(table, "", schemas);
+        schemas.put("shape", shape(table));
+        return schemas;
+    }
+
+    private static void captureRestSchemas(FileStoreTable table, String path, 
Map<String, String> schemas) {
+        if (table instanceof FallbackReadFileStoreTable) {
+            FallbackReadFileStoreTable pair = (FallbackReadFileStoreTable) 
table;
+            captureRestSchemas(pair.wrapped(), path, schemas);
+            captureRestSchemas(pair.other(), path + "fallback.", schemas);
+        } else if (table instanceof DelegatedFileStoreTable) {
+            captureRestSchemas(((DelegatedFileStoreTable) table).wrapped(), 
path, schemas);
+        } else {
+            SchemaManager manager = table.schemaManager();
+            schemas.put(path, restSchemaIdentity(manager, 
manager.schema(table.schema().id())));
+        }
+    }
+
+    static FileStoreTable bindRestSchema(Table refreshed, Map<String, String> 
schemas) {
+        if (!(refreshed instanceof FileStoreTable) || 
!shape(refreshed).equals(schemas.get("shape"))) {
+            throw changed();
+        }
+        return bindRestSchema((FileStoreTable) refreshed, "", schemas);
+    }
+
+    private static FileStoreTable bindRestSchema(FileStoreTable table, String 
path, Map<String, String> schemas) {
+        if (table instanceof FallbackReadFileStoreTable) {
+            FallbackReadFileStoreTable pair = (FallbackReadFileStoreTable) 
table;
+            return PaimonTableDecorators.rebuildFallbackPair(pair,
+                    bindRestSchema(pair.wrapped(), path, schemas),
+                    bindRestSchema(pair.other(), path + "fallback.", schemas));
+        }
+        if (table instanceof DelegatedFileStoreTable) {
+            return PaimonTableDecorators.replaceWrapped(table,
+                    bindRestSchema(((DelegatedFileStoreTable) 
table).wrapped(), path, schemas));
+        }
+        TableSchema retained = table.schema();
+        SchemaManager manager = table.schemaManager();
+        TableSchema persisted = manager.schema(retained.id());
+        // Bracket the fresh REST lookup with physical reads. The first REST 
handle only locates files;
+        // reusing its options could combine an old bucket key with a 
replacement's schema identity.
+        if (!restSchemaIdentity(manager, persisted).equals(schemas.get(path))) 
{
+            throw changed();
+        }
+        // REST synthesizes timeMillis, version and highestFieldId, so compare 
its logical schema only
+        // after confirming that the physical generation stayed unchanged 
across the REST lookup.
+        if (!retained.copy(Collections.emptyMap()).toSchema()
+                .equals(persisted.copy(Collections.emptyMap()).toSchema())) {
+            throw new DorisConnectorException("Paimon REST schema does not 
match stored schema "
+                    + retained.id() + "; retry the statement");
+        }
+        // Preserve the freshly loaded REST FileIO, catalog callbacks and 
options. Cache hits never rebind.
+        return table.copy(persisted.copy(retained.options()));
+    }
+
+    private static String restSchemaIdentity(SchemaManager manager, 
TableSchema schema) {
+        // Include the branch/path and all persisted options, even if a 
replacement reuses a timestamp.
+        return manager.toSchemaPath(schema.id()) + "\n" + schemaDigest(schema);
+    }
+
     static void validateRetainedSchema(TableSchema retained, TableSchema 
persisted) {
         // Dynamic copies legitimately overlay options, but preserve the 
schema's structural metadata
         // and creation timestamp. Compare those against the same ID's 
physical history, not latest.
diff --git 
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonRestSchemaTest.java
 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonRestSchemaTest.java
new file mode 100644
index 00000000000..5ad0f5f52f0
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonRestSchemaTest.java
@@ -0,0 +1,347 @@
+// 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.connector.paimon;
+
+import org.apache.doris.connector.cache.CatalogMetaCache;
+import org.apache.doris.connector.spi.DorisConnectorException;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.predicate.PredicateBuilder;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.rest.RESTCatalog;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.schema.TableSchema;
+import org.apache.paimon.table.CatalogEnvironment;
+import org.apache.paimon.table.FallbackReadFileStoreTable;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.FileStoreTableFactory;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.table.source.Split;
+import org.apache.paimon.types.DataTypes;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+class PaimonRestSchemaTest {
+    private static final Identifier TABLE = Identifier.create("db", "events");
+
+    @TempDir
+    java.nio.file.Path directory;
+
+    @Test
+    void restSchemaCanBePinnedWithCacheEnabledOrDisabled() throws Exception {
+        for (boolean cacheEnabled : new boolean[] {true, false}) {
+            FileStoreTable physical = physicalTable("table-" + cacheEnabled, 
"value", 1000L);
+            FileStoreTable restTable = restTable(physical, cacheEnabled ? 
"rest-table-id" : null);
+            Assertions.assertNotEquals(physical.schema().timeMillis(), 
restTable.schema().timeMillis());
+            Assertions.assertNotEquals(physical.schema().highestFieldId(), 
restTable.schema().highestFieldId());
+            Assertions.assertNotEquals(physical.schema().version(), 
restTable.schema().version());
+            AtomicReference<Table> response = new AtomicReference<>(restTable);
+            try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                    RESTCatalog rest = restCatalog(response, new 
AtomicInteger())) {
+                Catalog cached = cached(rest, owner, cacheEnabled);
+                FileStoreTable loaded = (FileStoreTable) 
cached.getTable(TABLE);
+                Map<String, String> pin = pin(loaded);
+                FileStoreTable scan = 
PaimonScanParams.applyOptionsWithoutTimeTravel(loaded, pin);
+                PaimonSchemaPin.validateRetainedSchema(scan.schema(), 
physical.schema());
+                Assertions.assertEquals("false", 
scan.options().get("file-index.read.enabled"));
+                Assertions.assertSame(restTable.fileIO(), scan.fileIO());
+                Assertions.assertSame(restTable.catalogEnvironment(), 
scan.catalogEnvironment());
+            }
+        }
+    }
+
+    @Test
+    void 
cachedRestHandleRejectsSameShapeReplacementEvenWhenPinDescribesReplacement() 
throws Exception {
+        FileStoreTable physical = physicalTable("replaced", "value", 1000L);
+        AtomicReference<Table> response = new 
AtomicReference<>(restTable(physical));
+        AtomicInteger loads = new AtomicInteger();
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(response, loads)) {
+            Catalog cached = cached(rest, owner, true);
+            FileStoreTable retained = (FileStoreTable) cached.getTable(TABLE);
+            physical.schemaManager().deleteSchema(0L);
+            FileStoreTable replacement = physicalTable("replaced", "value", 
2000L);
+            response.set(restTable(replacement));
+            Assertions.assertSame(retained, cached.getTable(TABLE));
+            Assertions.assertEquals(2, loads.get());
+            Map<String, String> pin = pin(retained);
+            Assertions.assertThrows(DorisConnectorException.class,
+                    () -> 
PaimonScanParams.applyOptionsWithoutTimeTravel(retained, pin));
+            cached.invalidateTable(TABLE);
+            FileStoreTable fresh = (FileStoreTable) cached.getTable(TABLE);
+            Assertions.assertDoesNotThrow(() -> 
PaimonScanParams.applyOptionsWithoutTimeTravel(fresh, pin));
+            Assertions.assertEquals(4, loads.get());
+        }
+    }
+
+    @Test
+    void restAndPhysicalFieldsMustAgreeBeforeBinding() throws Exception {
+        FileStoreTable physical = physicalTable("mismatch", "old_value", 
1000L);
+        FileStoreTable staleRest = restTable(physical);
+        physical.schemaManager().deleteSchema(0L);
+        physicalTable("mismatch", "new_value", 2000L);
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(new 
AtomicReference<>(staleRest), new AtomicInteger())) {
+            Catalog cached = cached(rest, owner, true);
+            Assertions.assertThrows(DorisConnectorException.class, () -> 
cached.getTable(TABLE));
+        }
+    }
+
+    @Test
+    void fallbackBranchesKeepTheirOwnPhysicalSchemas() throws Exception {
+        FileStoreTable main = physicalTable("main", "value", 1000L);
+        FileStoreTable fallback = physicalTable("fallback", "value", 2000L);
+        FallbackReadFileStoreTable pair = new 
FallbackReadFileStoreTable(restTable(main), restTable(fallback), true);
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(new AtomicReference<>(pair), 
new AtomicInteger())) {
+            FallbackReadFileStoreTable loaded = (FallbackReadFileStoreTable) 
cached(rest, owner, true).getTable(TABLE);
+            PaimonSchemaPin.validateRetainedSchema(loaded.wrapped().schema(), 
main.schema());
+            PaimonSchemaPin.validateRetainedSchema(loaded.other().schema(), 
fallback.schema());
+            Assertions.assertSame(pair.wrapped().catalogEnvironment(), 
loaded.wrapped().catalogEnvironment());
+            Assertions.assertSame(pair.other().catalogEnvironment(), 
loaded.other().catalogEnvironment());
+            Assertions.assertTrue(PaimonReaderOptions.isWrappedFirst(loaded));
+        }
+    }
+
+    @Test
+    void replacementBeforeFirstSchemaReadUsesFreshRestOptions() throws 
Exception {
+        FileStoreTable original = bucketTable("before-read");
+        FileStoreTable replacement = replacement(original, 2000L);
+        FileStoreTable oldRest = restTable(original);
+        FileStoreTable newRest = restTable(replacement);
+        AtomicInteger loads = new AtomicInteger();
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(() -> {
+                    if (loads.get() == 1) {
+                        replaceSchema(replacement);
+                        writeBucketRows(replacement);
+                        return oldRest;
+                    }
+                    return newRest;
+                }, loads)) {
+            FileStoreTable loaded = (FileStoreTable) cached(rest, owner, 
true).getTable(TABLE);
+            FileStoreTable mixed = 
oldRest.copy(replacement.schema().copy(oldRest.options()));
+            Assertions.assertTrue(matchingRows(mixed).size() < 16,
+                    "the fixture must expose bucket pruning with 
mixed-generation options");
+            Assertions.assertEquals(16, matchingRows(loaded).size(),
+                    "WHERE x = 1 must retain rows from every replacement 
bucket keyed by y");
+            Assertions.assertEquals("y", loaded.options().get("bucket-key"),
+                    "a replacement schema must never be combined with the 
original bucket key");
+            PaimonSchemaPin.validateRetainedSchema(loaded.schema(), 
replacement.schema());
+        }
+    }
+
+    @Test
+    void replacementDuringRestReloadRejectsTheLoadWithoutCachingIt() throws 
Exception {
+        for (long replacementTime : new long[] {1000L, 2000L}) {
+            FileStoreTable original = bucketTable("during-reload-" + 
replacementTime);
+            FileStoreTable replacement = replacement(original, 
replacementTime);
+            FileStoreTable oldRest = restTable(original);
+            FileStoreTable newRest = restTable(replacement);
+            AtomicInteger loads = new AtomicInteger();
+            try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                    RESTCatalog rest = restCatalog(() -> {
+                        if (loads.get() == 2) {
+                            replaceSchema(replacement);
+                        }
+                        return loads.get() <= 2 ? oldRest : newRest;
+                    }, loads)) {
+                Catalog cached = cached(rest, owner, true);
+                Assertions.assertThrows(DorisConnectorException.class, () -> 
cached.getTable(TABLE));
+                FileStoreTable retried = (FileStoreTable) 
cached.getTable(TABLE);
+                Assertions.assertEquals("y", 
retried.options().get("bucket-key"));
+                PaimonSchemaPin.validateRetainedSchema(retried.schema(), 
replacement.schema());
+            }
+        }
+    }
+
+    @Test
+    void fallbackReplacementDuringRestReloadIsAlsoRejected() throws Exception {
+        FileStoreTable main = bucketTable("stable-main");
+        FileStoreTable fallback = bucketTable("changing-fallback");
+        FileStoreTable replacement = replacement(fallback, 2000L);
+        FallbackReadFileStoreTable pair = new 
FallbackReadFileStoreTable(restTable(main), restTable(fallback), true);
+        AtomicInteger loads = new AtomicInteger();
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(() -> {
+                    if (loads.get() == 2) {
+                        replaceSchema(replacement);
+                    }
+                    return pair;
+                }, loads)) {
+            Catalog cached = cached(rest, owner, true);
+            Assertions.assertThrows(DorisConnectorException.class, () -> 
cached.getTable(TABLE));
+        }
+    }
+
+    @Test
+    void fallbackShapeChangeDuringReloadIsRejected() throws Exception {
+        FileStoreTable main = restTable(bucketTable("shape-main"));
+        FileStoreTable fallback = restTable(bucketTable("shape-fallback"));
+        AtomicInteger loads = new AtomicInteger();
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(() -> loads.get() == 1
+                        ? new FallbackReadFileStoreTable(main, fallback, true) 
: main, loads)) {
+            Assertions.assertThrows(DorisConnectorException.class, () -> 
cached(rest, owner, true).getTable(TABLE));
+        }
+    }
+
+    @Test
+    void changedLocationWithIdenticalSchemaDuringReloadIsRejected() throws 
Exception {
+        FileStoreTable original = restTable(bucketTable("original-location"));
+        FileStoreTable relocated = restTable(bucketTable("new-location"));
+        AtomicInteger loads = new AtomicInteger();
+        try (CatalogMetaCache owner = CatalogMetaCache.unmanaged();
+                RESTCatalog rest = restCatalog(() -> loads.get() == 1 ? 
original : relocated, loads)) {
+            Assertions.assertThrows(DorisConnectorException.class, () -> 
cached(rest, owner, true).getTable(TABLE));
+        }
+    }
+
+    private static void writeBucketRows(FileStoreTable table) {
+        BatchWriteBuilder builder = table.newBatchWriteBuilder();
+        try (BatchTableWrite write = builder.newWrite(); BatchTableCommit 
commit = builder.newCommit()) {
+            for (int y = 0; y < 16; y++) {
+                write.write(GenericRow.of(1, y));
+            }
+            commit.commit(write.prepareCommit());
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    private static List<Integer> matchingRows(FileStoreTable table) throws 
Exception {
+        ReadBuilder read = table.newReadBuilder().withFilter(new 
PredicateBuilder(table.rowType()).equal(0, 1));
+        List<Integer> values = new ArrayList<>();
+        for (Split split : read.newScan().plan().splits()) {
+            try (RecordReader<InternalRow> reader = 
read.newRead().createReader(split)) {
+                reader.forEachRemaining(row -> values.add(row.getInt(1)));
+            }
+        }
+        return values;
+    }
+
+    private FileStoreTable bucketTable(String name) throws Exception {
+        LocalFileIO io = LocalFileIO.create();
+        org.apache.paimon.fs.Path location = new 
org.apache.paimon.fs.Path(directory.resolve(name).toUri());
+        Schema schema = Schema.newBuilder().column("x", 
DataTypes.INT().notNull())
+                .column("y", DataTypes.INT().notNull()).primaryKey("x", "y")
+                .option("bucket", "2").option("bucket-key", "x").build();
+        TableSchema fields = TableSchema.create(0L, schema);
+        TableSchema persisted = new TableSchema(fields.version(), 0L, 
fields.fields(), fields.highestFieldId(),
+                fields.partitionKeys(), fields.primaryKeys(), 
fields.options(), fields.comment(), 1000L);
+        Assertions.assertTrue(new SchemaManager(io, 
location).commit(persisted));
+        return FileStoreTableFactory.create(io, location, persisted);
+    }
+
+    private static FileStoreTable replacement(FileStoreTable original, long 
time) {
+        TableSchema schema = original.schema();
+        Map<String, String> options = new HashMap<>(schema.options());
+        options.put("bucket-key", "y");
+        TableSchema replaced = new TableSchema(schema.version(), schema.id(), 
schema.fields(), schema.highestFieldId(),
+                schema.partitionKeys(), schema.primaryKeys(), options, 
schema.comment(), time);
+        return FileStoreTableFactory.create(original.fileIO(), 
original.location(), replaced);
+    }
+
+    private static void replaceSchema(FileStoreTable replacement) {
+        try {
+            
replacement.fileIO().overwriteFileUtf8(replacement.schemaManager().toSchemaPath(replacement.schema().id()),
+                    replacement.schema().toString());
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private FileStoreTable physicalTable(String name, String valueColumn, long 
creationTime) throws Exception {
+        LocalFileIO io = LocalFileIO.create();
+        org.apache.paimon.fs.Path location = new 
org.apache.paimon.fs.Path(directory.resolve(name).toUri());
+        Schema schema = Schema.newBuilder().column("id", DataTypes.INT())
+                .column(valueColumn, DataTypes.STRING()).option("bucket", 
"-1").build();
+        TableSchema fields = TableSchema.create(0L, schema);
+        // Older schema formats and removed field IDs are also absent from 
REST's Schema payload.
+        TableSchema persisted = new TableSchema(TableSchema.PAIMON_07_VERSION, 
0L, fields.fields(), 10,
+                fields.partitionKeys(), fields.primaryKeys(), 
fields.options(), fields.comment(), creationTime);
+        Assertions.assertTrue(new SchemaManager(io, 
location).commit(persisted));
+        return FileStoreTableFactory.create(io, location, persisted);
+    }
+
+    private static FileStoreTable restTable(FileStoreTable physical) {
+        return restTable(physical, "rest-table-id");
+    }
+
+    private static FileStoreTable restTable(FileStoreTable physical, String 
uuid) {
+        TableSchema reconstructed = TableSchema.create(physical.schema().id(), 
physical.schema().toSchema());
+        Map<String, String> options = new HashMap<>(reconstructed.options());
+        options.put("file-index.read.enabled", "false");
+        return FileStoreTableFactory.create(physical.fileIO(), 
physical.location(), reconstructed.copy(options),
+                new CatalogEnvironment(TABLE, uuid, null, null, null, null, 
false, true));
+    }
+
+    private static Map<String, String> pin(FileStoreTable table) {
+        Map<String, String> options = PaimonScanParams.withBoundSchema(
+                PaimonScanParams.pinOptionsToSnapshot(Collections.emptyMap(), 
-1L), table.schema().id());
+        options.putAll(PaimonSchemaPin.capture(table, table.schema().id(), 
-1L));
+        return options;
+    }
+
+    private static Catalog cached(RESTCatalog rest, CatalogMetaCache owner, 
boolean enabled) {
+        return PaimonMetaCacheCatalog.tryToCreate(rest, owner, 100, 100, new 
Options(),
+                enabled, true, catalog -> catalog);
+    }
+
+    private static RESTCatalog restCatalog(AtomicReference<Table> response, 
AtomicInteger loads) {
+        return restCatalog(response::get, loads);
+    }
+
+    private static RESTCatalog restCatalog(Supplier<Table> response, 
AtomicInteger loads) {
+        Options options = new Options();
+        options.set("uri", "http://localhost:1";);
+        options.set("prefix", "test-prefix");
+        options.set("token.provider", "bear");
+        options.set("token", "test-token");
+        return new RESTCatalog(CatalogContext.create(options), false) {
+            @Override
+            public Table getTable(Identifier identifier) {
+                loads.incrementAndGet();
+                return response.get();
+            }
+        };
+    }
+}
diff --git 
a/regression-test/suites/external_table_p2/paimon/test_paimon_rest_catalog_write.groovy
 
b/regression-test/suites/external_table_p2/paimon/test_paimon_rest_catalog_write.groovy
index 40f04e5b48a..f316089b0cc 100644
--- 
a/regression-test/suites/external_table_p2/paimon/test_paimon_rest_catalog_write.groovy
+++ 
b/regression-test/suites/external_table_p2/paimon/test_paimon_rest_catalog_write.groovy
@@ -73,6 +73,11 @@ suite("test_paimon_rest_catalog_write", "p2,external") {
         ORDER BY id
     """
 
+    // REST reloads reconstruct schema metadata; an unchanged table must keep 
the same readable rows.
+    def appendRows = sql """SELECT id, name, score, region FROM 
`${appendTable}` ORDER BY id"""
+    sql """REFRESH TABLE `${appendTable}`"""
+    assertEquals(appendRows, sql("""SELECT id, name, score, region FROM 
`${appendTable}` ORDER BY id"""))
+
     sql """
         CREATE TABLE `${primaryKeyTable}` (
             user_id INT NOT NULL,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to