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

morningman 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 d420f9c41bb [fix](fe) Load FE images that carry Lance catalogs written 
by 4.1.4+ (#69575)
d420f9c41bb is described below

commit d420f9c41bb657c661231f1f4274ac7aae19f0a7
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Oct 29 05:04:08 2026 +0000

    [fix](fe) Load FE images that carry Lance catalogs written by 4.1.4+ 
(#69575)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #65730, #67235, #68781
    
    Problem Summary:
    
    **In short.** This PR makes master read the Lance catalogs that
    branch-4.1 persists (#65730, released in 4.1.4): an FE image written by
    4.1.4 or later with a Lance catalog in it now loads. Before this PR the
    image failed to parse, so a cluster with a Lance catalog could not be
    upgraded from any released 4.1.4 / 4.1.4.1 / 4.1.5 build (or from 4.2)
    to a master-based build: the FE did not start. The Lance catalog's GSON
    labels are mapped to the plugin-driven classes, as the Iceberg, Paimon
    and LakeSoul labels already are, and the catalog stays registered,
    unusable until a Lance connector plugin is installed.
    
    **Background.** branch-4.1 added a Lance catalog as fe-core classes in
    #65730 (released in 4.1.4). Like every external catalog, it is persisted
    in the image's `datasource` module as GSON JSON whose `clazz` field
    names the class, `LanceExternalCatalog`, with `logType` `LANCE`. master
    has no Lance classes: external catalogs are served by connector plugins,
    and the labels of the catalogs that used to be fe-core classes are
    redirected to `PluginDrivenExternalCatalog` /
    `PluginDrivenExternalDatabase` / `PluginDrivenMvccExternalTable` by
    `registerCompatibleSubtype` in `GsonUtils`. A Lance connector plugin
    does not exist on master yet; it will be ported later.
    
    branch-4.1 also had, for a while, a durable Lance index job framework
    (#67235) that wrote edit-log op 500 (`OP_LANCE_INDEX_JOB_UPSERT`) and an
    image module named `lanceIndexJobManager`. #68781 reverted it on
    branch-4.1 and branch-4.2 before any release: among the release tags
    only 4.1.5-rc01 has the writer, while 4.1.5 and 4.2.0-rc01 keep just a
    tombstone that reads and discards both. master never had the framework,
    and this PR does not make it read that metadata either.
    
    The upgrade fixtures on master (`upgrade/413`) were generated from
    4.1.3, before Lance existed, so no test caught this.
    
    **The problem, and what it cost.** Compared on master against fixtures
    emitted by real 4.1.5 bytecode:
    
    | Metadata from a 4.1.4+ FE | master before this PR | after this PR |
    |---|---|---|
    | image whose `datasource` module has a Lance catalog (every 4.1.4+
    cluster with one) | image fails to parse: `cannot deserialize interface
    org.apache.doris.datasource.CatalogIf subtype named
    LanceExternalCatalog; did you forget to register a subtype?`; the FE
    cannot start | loads; the catalog is a `PluginDrivenExternalCatalog` of
    type `lance` with all its properties, comment and auto-analyze policies;
    using it fails with `No ConnectorProvider found for plugin-driven
    catalog: <name>, type: lance. Ensure the connector plugin is installed.`
    |
    | `OP_CREATE_CATALOG` of a Lance catalog | replays as a degraded catalog
    (the entry carries only properties) | unchanged |
    
    The only labels in the GSON registries of 4.1.4.1, 4.1.5 and 4.2.0-rc01
    that master does not know are `LanceExternalCatalog`,
    `LanceExternalDatabase` and `LanceExternalTable`.
    
    **How this PR fixes it.**
    
    - `GsonUtils`: `LanceExternalCatalog` → `PluginDrivenExternalCatalog`,
    `LanceExternalDatabase` → `PluginDrivenExternalDatabase`,
    `LanceExternalTable` → `PluginDrivenMvccExternalTable`
    (`LanceExternalTable` implemented `MvccTable`). 4.1.x never persists an
    external database or table, so only the catalog label can actually
    appear; the other two complete the registry, as for the other migrated
    connectors.
    - `InitCatalogLog.Type` keeps `LANCE`, so the `logType` 4.1.4+ wrote
    still deserializes to itself. Nothing depends on it: a Lance catalog
    always persists its `type` property (no resource type creates one), and
    `PluginDrivenExternalCatalog#gsonPostProcess` rewrites the logType to
    `PLUGIN`.
    - `BdbjeJournalDataProcNode`, which renders `SHOW PROC
    '/bdbje/<db>/<journalId>'` in BDBJE debug mode, prints a record read
    without a payload as `null`, as `BDBTool` already does, instead of
    failing with a `NullPointerException`. This is an existing master issue
    that came up in review, not a Lance one: the readers of the retired
    operation types already on master (the PL/SQL ones of #58700 and the
    external-catalog ones) keep no payload.
    
    Until a Lance connector plugin is installed, an upgraded cluster lists
    its Lance catalogs and fails only when one is used; installing the
    plugin makes them work without any metadata change, since the catalog
    properties are handed to the connector unchanged.
    
    **Results.**
    
    - New golden fixtures `fe/fe-core/src/test/resources/upgrade/415/`,
    emitted by 4.1.5-release bytecode like `upgrade/413` (generator and
    `PROVENANCE.txt` included, byte-deterministic): the `datasource` module
    with three Lance catalogs (filesystem on S3, filesystem on `file://`,
    REST with bearer auth), their `OP_CREATE_CATALOG` entries, and 4.1.5's
    label registries.
    - `Legacy415LanceUpgradeTest` (7 tests): the image loads and every Lance
    catalog becomes a `PluginDrivenExternalCatalog` of type `lance` with the
    `PLUGIN` logType; every persisted property, the comment,
    `lastUpdateTime` and the auto-analyze policy survive; the persisted
    logType names still deserialize; without a plugin the catalog fails only
    when used, with the message above; a provider claiming `lance` receives
    exactly the persisted properties, from the image and from the journal;
    the journal entries replay with and without a plugin.
    - `LanceGsonCompatReplayTest` (4 tests): the three labels replay as the
    expected classes (the table as the MVCC variant), and they are the only
    labels 4.1.5 registered beyond 4.1.3.
    - Each of these changes, undone on its own, turns tests red: dropping
    the catalog label (5 tests, with the parse error above), the database
    label, mapping the table label to the non-MVCC class, or removing
    `LANCE`.
    - `BdbjeJournalDataProcNodeTest` (2 tests): a retired PL/SQL record, in
    its written form, shows its journal ID, operation name, size and `null`
    data in the debug view; a live `OP_SAVE_NEXTID` record still shows its
    payload. With `getData().toString()` back, the first test fails with a
    `NullPointerException`.
    - The three classes above with 13 related upgrade, GSON and journal
    suites (`Legacy413ImageMigrationTest`, `*GsonCompatReplayTest`,
    `LegacyExternalTableTypeReplayTest`, `CatalogMgrTest`,
    `ExternalCatalogTest`, `ConstraintPersistTest`, `BDBJEJournalTest`,
    `BDBToolTest`, `BDBDebuggerTest`, ...): 103 tests pass; fe-core
    checkstyle passes.
    
    Co-authored-by: kid <[email protected]>
---
 .licenserc.yaml                                    |   7 +
 .../common/proc/BdbjeJournalDataProcNode.java      |   4 +-
 .../doris/datasource/log/InitCatalogLog.java       |   5 +
 .../org/apache/doris/persist/gson/GsonUtils.java   |  16 +-
 .../common/proc/BdbjeJournalDataProcNodeTest.java  | 121 ++++++++++
 .../datasource/LanceGsonCompatReplayTest.java      | 121 ++++++++++
 .../apache/doris/datasource/Legacy413Fixtures.java |  46 ++--
 .../apache/doris/datasource/Legacy415Fixtures.java |  74 ++++++
 .../datasource/Legacy415LanceUpgradeTest.java      | 227 +++++++++++++++++++
 .../src/test/resources/upgrade/415/PROVENANCE.txt  |  56 +++++
 .../src/test/resources/upgrade/415/catalogMgr.json |   1 +
 .../resources/upgrade/415/datasource.module.bin    | Bin 0 -> 1302 bytes
 .../src/test/resources/upgrade/415/editlog.index   |   4 +
 .../415/editlog/op320-create-g1_lance_local.bin    | Bin 0 -> 222 bytes
 .../415/editlog/op320-create-g1_lance_rest.bin     | Bin 0 -> 364 bytes
 .../415/editlog/op320-create-g1_lance_s3.bin       | Bin 0 -> 354 bytes
 .../upgrade/415/generator/Gen415Fixtures.java      | 248 +++++++++++++++++++++
 .../src/test/resources/upgrade/415/labels.db.txt   |  15 ++
 .../src/test/resources/upgrade/415/labels.ds.txt   |  26 +++
 .../src/test/resources/upgrade/415/labels.tbl.txt  |  29 +++
 20 files changed, 984 insertions(+), 16 deletions(-)

diff --git a/.licenserc.yaml b/.licenserc.yaml
index 4d2bf21c833..e28192e8df0 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -96,6 +96,13 @@ header:
     - "fe/fe-core/src/test/resources/upgrade/413/labels.db.txt"
     - "fe/fe-core/src/test/resources/upgrade/413/labels.ds.txt"
     - "fe/fe-core/src/test/resources/upgrade/413/labels.tbl.txt"
+    # Same for the golden 4.1.5 (Lance catalog) fixtures emitted by 
Gen415Fixtures;
+    # upgrade/415/generator/Gen415Fixtures.java stays checked.
+    - "fe/fe-core/src/test/resources/upgrade/415/PROVENANCE.txt"
+    - "fe/fe-core/src/test/resources/upgrade/415/editlog.index"
+    - "fe/fe-core/src/test/resources/upgrade/415/labels.db.txt"
+    - "fe/fe-core/src/test/resources/upgrade/415/labels.ds.txt"
+    - "fe/fe-core/src/test/resources/upgrade/415/labels.tbl.txt"
     - 
"fe/fe-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java"
     - "fe/fe-core/src/main/antlr4/org/apache/doris/nereids/JavaLexer.g4"
     - "fe/fe-core/src/main/antlr4/org/apache/doris/nereids/JavaParser.g4"
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/common/proc/BdbjeJournalDataProcNode.java
 
b/fe/fe-core/src/main/java/org/apache/doris/common/proc/BdbjeJournalDataProcNode.java
index 6b18bd05a46..6f29d4a37d1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/common/proc/BdbjeJournalDataProcNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/common/proc/BdbjeJournalDataProcNode.java
@@ -52,7 +52,9 @@ public class BdbjeJournalDataProcNode implements 
ProcNodeInterface  {
         BDBDebugger.JournalEntityWrapper entity = env.getJournalEntity(dbName, 
journalId);
 
         short opCode = entity.entity == null ? -1 : entity.entity.getOpCode();
-        String data = entity.entity == null ? entity.errMsg : 
entity.entity.getData().toString();
+        // The readers of retired operation types (e.g. the PL/SQL ones) 
consume the payload without keeping it;
+        // print such a record the way BDBTool does.
+        String data = entity.entity == null ? entity.errMsg : 
String.valueOf(entity.entity.getData());
         result.addRow(Lists.newArrayList(entity.journalId.toString(),
                     OperationType.getOpName(opCode), entity.size.toString(), 
data));
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/log/InitCatalogLog.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/log/InitCatalogLog.java
index f97d5cd04c4..6ae6ab508f3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/log/InitCatalogLog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/log/InitCatalogLog.java
@@ -42,6 +42,10 @@ public class InitCatalogLog implements Writable {
      *
      * <p>HUDI is absent because it was never produced: hudi tables have 
always lived in an hms catalog and
      * no {@code HudiExternalCatalog} ever existed, so no image can carry 
it.</p>
+     *
+     * <p>LANCE is written by the 4.1.4+ FEs that shipped a Lance catalog. A 
Lance catalog always persists its
+     * {@code type} property (no resource type creates one), so nothing is 
backfilled from it; it stays so
+     * that every name a released image carries still deserializes to 
itself.</p>
      */
     public enum Type {
         HMS,
@@ -54,6 +58,7 @@ public class InitCatalogLog implements Writable {
         TEST,
         TRINO_CONNECTOR,
         REMOTE_DORIS,
+        LANCE,
         PLUGIN,
         UNKNOWN;
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/persist/gson/GsonUtils.java 
b/fe/fe-core/src/main/java/org/apache/doris/persist/gson/GsonUtils.java
index 264f98cbdeb..ad05a6e0f32 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/persist/gson/GsonUtils.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/persist/gson/GsonUtils.java
@@ -413,7 +413,12 @@ public class GsonUtils {
                 // was never migrated to a connector, so the remapped catalog 
has no backing connector and
                 // errors on access (it must be dropped); this only keeps old 
images/edit-logs loadable.
                 .registerCompatibleSubtype(
-                        PluginDrivenExternalCatalog.class, 
"LakeSoulExternalCatalog");
+                        PluginDrivenExternalCatalog.class, 
"LakeSoulExternalCatalog")
+                // Migrate Lance catalogs written by 4.1.4+ (4.1.x / 4.2) to 
PluginDriven on deserialization.
+                // Until a lance connector plugin is installed the catalog has 
no backing connector and errors
+                // on access; without this label the whole image fails to 
parse and the FE cannot start.
+                .registerCompatibleSubtype(
+                        PluginDrivenExternalCatalog.class, 
"LanceExternalCatalog");
         if (Config.isNotCloudMode()) {
             dsTypeAdapterFactory
                     .registerSubtype(InternalCatalog.class, 
InternalCatalog.class.getSimpleName());
@@ -472,7 +477,10 @@ public class GsonUtils {
                     PluginDrivenExternalDatabase.class, "HMSExternalDatabase")
             // Migrate old LakeSoul databases to PluginDriven on 
deserialization (deprecated, no connector)
             .registerCompatibleSubtype(
-                    PluginDrivenExternalDatabase.class, 
"LakeSoulExternalDatabase");
+                    PluginDrivenExternalDatabase.class, 
"LakeSoulExternalDatabase")
+            // Migrate Lance databases (4.1.4+) to PluginDriven on 
deserialization
+            .registerCompatibleSubtype(
+                    PluginDrivenExternalDatabase.class, 
"LanceExternalDatabase");
 
     private static RuntimeTypeAdapterFactory<TableIf> tblTypeAdapterFactory = 
RuntimeTypeAdapterFactory.of(
                     TableIf.class, 
"clazz").registerSubtype(ExternalTable.class, 
ExternalTable.class.getSimpleName())
@@ -508,6 +516,10 @@ public class GsonUtils {
             // PluginDrivenMvccExternalTable — matching what 
buildTableInternal rebuilds it as on replay.
             .registerCompatibleSubtype(
                     PluginDrivenMvccExternalTable.class, "HMSExternalTable")
+            // Lance tables (4.1.4+) migrate to the MVCC variant: 
LanceExternalTable implemented MvccTable
+            // (dataset versions back snapshots/time-travel)
+            .registerCompatibleSubtype(
+                    PluginDrivenMvccExternalTable.class, "LanceExternalTable")
             .registerSubtype(BrokerTable.class, 
BrokerTable.class.getSimpleName())
             .registerSubtype(EsTable.class, EsTable.class.getSimpleName())
             .registerSubtype(FunctionGenTable.class, 
FunctionGenTable.class.getSimpleName())
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/common/proc/BdbjeJournalDataProcNodeTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/common/proc/BdbjeJournalDataProcNodeTest.java
new file mode 100644
index 00000000000..95c7ac0d0bc
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/common/proc/BdbjeJournalDataProcNodeTest.java
@@ -0,0 +1,121 @@
+// 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.common.proc;
+
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.Config;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.journal.JournalEntity;
+import org.apache.doris.journal.bdbje.BDBDebugger;
+import org.apache.doris.persist.OperationType;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * SHOW PROC '/bdbje/dbName/journalId', the BDBJE debug-mode view of a single 
journal record.
+ */
+public class BdbjeJournalDataProcNodeTest {
+    private static final String DB_NAME = "1";
+
+    private boolean debugMode;
+    private BDBDebugger.BDBDebugEnv debugEnv;
+
+    @BeforeEach
+    public void setUp() {
+        debugMode = Config.enable_bdbje_debug_mode;
+        debugEnv = BDBDebugger.get().getEnv();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        Config.enable_bdbje_debug_mode = debugMode;
+        Deencapsulation.setField(BDBDebugger.get(), "debugEnv", debugEnv);
+    }
+
+    @Test
+    public void recordOfARetiredOperationTypeIsPrintable() throws Exception {
+        // The readers of retired operation types consume the payload without 
keeping it, e.g. the PL/SQL ones
+        // that #58700 retired, whose records are Text-framed JSON.
+        BDBDebugger.JournalEntityWrapper plsqlPackage = record(8L, 
OperationType.OP_DROP_PLSQL_PACKAGE,
+                "{\"name\":\"pkg1\",\"catalogId\":0,\"dbName\":\"db1\"}");
+        Assertions.assertNull(plsqlPackage.entity.getData());
+        installDebugEnv(plsqlPackage);
+
+        // MUTATION: with getData().toString() the row fails with a 
NullPointerException.
+        Assertions.assertEquals(Lists.newArrayList("8", 
"OP_DROP_PLSQL_PACKAGE",
+                plsqlPackage.size.toString(), "null"), fetchRow(8L));
+    }
+
+    @Test
+    public void liveRecordStillShowsItsPayload() throws Exception {
+        BDBDebugger.JournalEntityWrapper nextId = record(9L, 
OperationType.OP_SAVE_NEXTID, "12345");
+        installDebugEnv(nextId);
+
+        Assertions.assertEquals(Lists.newArrayList("9", "OP_SAVE_NEXTID", 
nextId.size.toString(), "12345"),
+                fetchRow(9L));
+    }
+
+    /** A journal record as BDBDebugEnv#getJournalEntity reads it back from 
BDBJE. */
+    private static BDBDebugger.JournalEntityWrapper record(long journalId, 
short opCode, String payload)
+            throws IOException {
+        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+        try (DataOutputStream out = new DataOutputStream(bytes)) {
+            out.writeShort(opCode);
+            // The int-length framing that both Text#write and 
Text.writeString produce.
+            Text.writeString(out, payload);
+        }
+        BDBDebugger.JournalEntityWrapper wrapper = new 
BDBDebugger.JournalEntityWrapper(journalId);
+        wrapper.size = bytes.size();
+        JournalEntity entity = new JournalEntity();
+        try (DataInputStream in = new DataInputStream(new 
ByteArrayInputStream(bytes.toByteArray()))) {
+            entity.readFields(in);
+            Assertions.assertEquals(-1, in.read(), "the record must be 
consumed exactly");
+        }
+        wrapper.entity = entity;
+        return wrapper;
+    }
+
+    private static void installDebugEnv(BDBDebugger.JournalEntityWrapper... 
records) {
+        BDBDebugger.BDBDebugEnv env = 
Mockito.mock(BDBDebugger.BDBDebugEnv.class);
+        for (BDBDebugger.JournalEntityWrapper record : records) {
+            Mockito.when(env.getJournalEntity(DB_NAME, 
record.journalId)).thenReturn(record);
+        }
+        Deencapsulation.setField(BDBDebugger.get(), "debugEnv", env);
+        Config.enable_bdbje_debug_mode = true;
+    }
+
+    private static List<String> fetchRow(long journalId) throws 
AnalysisException {
+        ProcResult result = new BdbjeJournalDataProcNode(DB_NAME, 
journalId).fetchResult();
+        Assertions.assertEquals(BdbjeJournalDataProcNode.TITLE_NAMES, 
result.getColumnNames());
+        Assertions.assertEquals(1, result.getRows().size());
+        return result.getRows().get(0);
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/LanceGsonCompatReplayTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/LanceGsonCompatReplayTest.java
new file mode 100644
index 00000000000..e54b03a759b
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/LanceGsonCompatReplayTest.java
@@ -0,0 +1,121 @@
+// 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;
+
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.datasource.mvcc.PluginDrivenMvccExternalTable;
+import org.apache.doris.datasource.plugin.PluginDrivenExternalCatalog;
+import org.apache.doris.datasource.plugin.PluginDrivenExternalDatabase;
+import org.apache.doris.persist.gson.GsonUtils;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Guards loading the Lance catalogs/databases/tables that 4.1.4+ FEs 
persisted (4.1.x and 4.2 shipped Lance as
+ * fe-core classes; this branch has no such classes). Their GSON "clazz" 
labels must be redirected by the
+ * {@code registerCompatibleSubtype} mappings in {@link GsonUtils} to the 
generic PluginDriven classes —
+ * otherwise an image from a cluster with a Lance catalog fails with a {@code 
JsonParseException} (label not
+ * registered) and the FE cannot start.
+ *
+ * <p>The table label lands on the MVCC variant: {@code LanceExternalTable} 
implemented {@code MvccTable}
+ * (dataset versions back snapshots and time travel), so replaying it as the 
base
+ * {@code PluginDrivenExternalTable} would silently downgrade it.</p>
+ *
+ * <p>Each case round-trips a valid PluginDriven object through GSON and 
rewrites only the "clazz" discriminator
+ * to the legacy label, like {@link PaimonGsonCompatReplayTest}. The real 
4.1.5 bytes of a Lance catalog are
+ * covered by {@link Legacy415LanceUpgradeTest}; a database or table is never 
persisted by 4.1.5.</p>
+ */
+public class LanceGsonCompatReplayTest {
+
+    private static String swapClazz(String json, String currentTag, String 
legacyTag) {
+        String needle = "\"clazz\":\"" + currentTag + "\"";
+        // Sanity: the polymorphic serialization must emit the discriminator 
we are about to rewrite.
+        Assertions.assertTrue(json.contains(needle),
+                "expected discriminator " + needle + " in serialized json: " + 
json);
+        return json.replace(needle, "\"clazz\":\"" + legacyTag + "\"");
+    }
+
+    @Test
+    public void testLegacyLanceCatalogTagReplaysAsPluginDriven() {
+        Map<String, String> props = Maps.newHashMap();
+        props.put("type", "lance");
+        PluginDrivenExternalCatalog catalog =
+                new PluginDrivenExternalCatalog(1L, "lance_ctl", "", props, 
"c", null);
+        String baseJson = GsonUtils.GSON.toJson(catalog, CatalogIf.class);
+
+        String json = swapClazz(baseJson, "PluginDrivenExternalCatalog", 
"LanceExternalCatalog");
+        // MUTATION: removing the registerCompatibleSubtype throws "cannot 
deserialize ... subtype named
+        // LanceExternalCatalog" here; a wrong target class fails the 
assertSame.
+        CatalogIf<?> restored = GsonUtils.GSON.fromJson(json, CatalogIf.class);
+        Assertions.assertSame(PluginDrivenExternalCatalog.class, 
restored.getClass(),
+                "legacy 'LanceExternalCatalog' tag must replay as 
PluginDrivenExternalCatalog");
+    }
+
+    @Test
+    public void testLegacyLanceDatabaseTagReplaysAsPluginDriven() {
+        PluginDrivenExternalDatabase db = new PluginDrivenExternalDatabase();
+        db.id = 2L;
+        db.name = "lance_db";
+        String baseJson = GsonUtils.GSON.toJson(db, DatabaseIf.class);
+
+        String json = swapClazz(baseJson, "PluginDrivenExternalDatabase", 
"LanceExternalDatabase");
+        // MUTATION: dropping the db registerCompatibleSubtype makes this 
throw on deserialize.
+        DatabaseIf<?> restored = GsonUtils.GSON.fromJson(json, 
DatabaseIf.class);
+        Assertions.assertSame(PluginDrivenExternalDatabase.class, 
restored.getClass(),
+                "legacy 'LanceExternalDatabase' tag must replay as 
PluginDrivenExternalDatabase");
+    }
+
+    @Test
+    public void testLegacyLanceTableTagReplaysAsMvccPluginDriven() {
+        PluginDrivenMvccExternalTable table = new 
PluginDrivenMvccExternalTable();
+        table.id = 3L;
+        table.name = "lance_tbl";
+        table.dbName = "lance_db";
+        String baseJson = GsonUtils.GSON.toJson(table, TableIf.class);
+
+        String json = swapClazz(baseJson, "PluginDrivenMvccExternalTable", 
"LanceExternalTable");
+        TableIf restored = GsonUtils.GSON.fromJson(json, TableIf.class);
+        // assertSame, not instanceof: a mapping to the base 
PluginDrivenExternalTable would also pass instanceof.
+        Assertions.assertSame(PluginDrivenMvccExternalTable.class, 
restored.getClass(),
+                "legacy 'LanceExternalTable' tag must replay as 
PluginDrivenMvccExternalTable,"
+                        + " not the base PluginDrivenExternalTable");
+    }
+
+    @Test
+    public void lanceLabelsAreTheOnlyOnesAReleaseAddedAfter413() throws 
Exception {
+        // The three cases above are complete only if Lance is all that 
4.1.5's registries added on top of
+        // 4.1.3's (which Legacy413LabelRegistryTest covers label by label). 
Both inventories are dumped
+        // reflectively by the fixture generators, so a label added in a later 
release cannot hide here.
+        Assertions.assertEquals(ImmutableSet.of("LanceExternalCatalog"), 
addedSince413("ds"));
+        Assertions.assertEquals(ImmutableSet.of("LanceExternalDatabase"), 
addedSince413("db"));
+        Assertions.assertEquals(ImmutableSet.of("LanceExternalTable"), 
addedSince413("tbl"));
+    }
+
+    private static Set<String> addedSince413(String which) throws Exception {
+        return Sets.difference(Legacy415Fixtures.legacyLabels(which).keySet(),
+                Legacy413Fixtures.legacyLabels(which).keySet());
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy413Fixtures.java 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy413Fixtures.java
index 5f8f2c33d3b..a9482611c95 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy413Fixtures.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy413Fixtures.java
@@ -34,6 +34,9 @@ import java.util.Map;
  * <p>Those files were produced by real 4.1.3 bytecode, not written by hand; 
see the PROVENANCE.txt
  * next to them for how to regenerate and for the five wire-format details 
that make hand-authored
  * fixtures wrong.
+ *
+ * <p>The package-private overloads taking a resource root are shared with the 
fixture families of later
+ * releases that persisted new labels ({@link Legacy415Fixtures}), which are 
generated the same way.
  */
 public final class Legacy413Fixtures {
 
@@ -56,7 +59,11 @@ public final class Legacy413Fixtures {
     }
 
     private static CatalogMgr loadCatalogMgr(String fileName) throws 
IOException {
-        try (InputStream in = open(fileName);
+        return loadCatalogMgr(ROOT, fileName);
+    }
+
+    static CatalogMgr loadCatalogMgr(String root, String fileName) throws 
IOException {
+        try (InputStream in = open(root, fileName);
                 DataInputStream dis = new DataInputStream(in)) {
             return CatalogMgr.read(dis);
         }
@@ -64,32 +71,34 @@ public final class Legacy413Fixtures {
 
     /** Raw bytes of one edit-log entry, in JournalEntity wire form (opCode 
short + Text-framed JSON). */
     public static byte[] journalEntry(String fileName) throws IOException {
-        try (InputStream in = open("editlog/" + fileName)) {
-            return readAll(in);
-        }
+        return resource(ROOT, "editlog/" + fileName);
     }
 
     /** Names of every edit-log fixture with the given op code, e.g. {@code 
320}. */
     public static List<String> journalEntryNames(int opCode) {
+        return journalEntryNames(ROOT, opCode);
+    }
+
+    static List<String> journalEntryNames(String root, int opCode) {
         List<String> names = new ArrayList<>();
-        for (String name : listEditLogFixtures()) {
+        for (String name : listEditLogFixtures(root)) {
             if (name.startsWith("op" + opCode + "-")) {
                 names.add(name);
             }
         }
         if (names.isEmpty()) {
             throw new IllegalStateException("no edit-log fixture for op " + 
opCode
-                    + "; the resource index at " + ROOT + "editlog.index is 
stale");
+                    + "; the resource index at " + root + "editlog.index is 
stale");
         }
         return names;
     }
 
-    private static List<String> listEditLogFixtures() {
+    private static List<String> listEditLogFixtures(String root) {
         // Resource directories are not enumerable from a jar, and surefire 
may well run these classes off one.
         // The generator writes a flat index next to the fixtures precisely so 
this does not need directory
         // listing; keeping it in one place means a fixture added without 
regenerating the index fails loudly
         // in journalEntryNames() rather than silently reducing coverage.
-        try (InputStream in = open("editlog.index")) {
+        try (InputStream in = open(root, "editlog.index")) {
             String text = new String(readAll(in), StandardCharsets.UTF_8);
             List<String> names = new ArrayList<>();
             for (String line : text.split("\n")) {
@@ -111,8 +120,12 @@ public final class Legacy413Fixtures {
      * @return label to the fully-qualified name of the class 4.1.3 mapped it 
to, in file order
      */
     public static Map<String, LegacyLabel> legacyLabels(String which) throws 
IOException {
+        return legacyLabels(ROOT, which);
+    }
+
+    static Map<String, LegacyLabel> legacyLabels(String root, String which) 
throws IOException {
         Map<String, LegacyLabel> labels = new LinkedHashMap<>();
-        try (InputStream in = open("labels." + which + ".txt")) {
+        try (InputStream in = open(root, "labels." + which + ".txt")) {
             String text = new String(readAll(in), StandardCharsets.UTF_8);
             for (String line : text.split("\n")) {
                 if (line.trim().isEmpty() || line.startsWith("#")) {
@@ -148,11 +161,18 @@ public final class Legacy413Fixtures {
         }
     }
 
-    private static InputStream open(String relative) throws IOException {
-        InputStream in = Legacy413Fixtures.class.getResourceAsStream(ROOT + 
relative);
+    /** All bytes of one fixture file under the release directory {@code 
root}. */
+    static byte[] resource(String root, String relative) throws IOException {
+        try (InputStream in = open(root, relative)) {
+            return readAll(in);
+        }
+    }
+
+    private static InputStream open(String root, String relative) throws 
IOException {
+        InputStream in = Legacy413Fixtures.class.getResourceAsStream(root + 
relative);
         if (in == null) {
-            throw new IOException("missing 4.1.3 fixture: " + ROOT + relative
-                    + " -- see src/test/resources/upgrade/413/PROVENANCE.txt");
+            throw new IOException("missing upgrade fixture: " + root + relative
+                    + " -- see src/test/resources" + root + "PROVENANCE.txt");
         }
         return in;
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy415Fixtures.java 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy415Fixtures.java
new file mode 100644
index 00000000000..4a790acdf42
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy415Fixtures.java
@@ -0,0 +1,74 @@
+// 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;
+
+import org.apache.doris.common.io.Text;
+import org.apache.doris.datasource.Legacy413Fixtures.LegacyLabel;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Access to the golden Doris 4.1.5 metadata fixtures under {@code 
src/test/resources/upgrade/415/}: the Lance
+ * catalogs 4.1.4 introduced, in the bytes a real 4.1.5 FE writes (see the 
PROVENANCE.txt next to them).
+ */
+public final class Legacy415Fixtures {
+
+    /** The lastUpdateTime the generator stamps on every catalog, so the bytes 
are deterministic. */
+    public static final long FIXED_UPDATE_TIME = 1753000000000L;
+
+    private static final String ROOT = "/upgrade/415/";
+
+    private Legacy415Fixtures() {
+    }
+
+    /** The image's "datasource" module as a 4.1.5 FE would have written it. */
+    public static CatalogMgr loadCatalogMgr() throws IOException {
+        return Legacy413Fixtures.loadCatalogMgr(ROOT, "datasource.module.bin");
+    }
+
+    /** The same module's JSON payload, unframed, for assertions on what is 
literally on the wire. */
+    public static String catalogMgrJson() throws IOException {
+        byte[] framed = Legacy413Fixtures.resource(ROOT, 
"datasource.module.bin");
+        try (DataInputStream in = new DataInputStream(new 
ByteArrayInputStream(framed))) {
+            return Text.readString(in);
+        }
+    }
+
+    /** Raw bytes of one edit-log entry, in JournalEntity wire form (opCode 
short + Text-framed JSON). */
+    public static byte[] journalEntry(String fileName) throws IOException {
+        return Legacy413Fixtures.resource(ROOT, "editlog/" + fileName);
+    }
+
+    /** Names of every edit-log fixture with the given op code, e.g. {@code 
320}. */
+    public static List<String> journalEntryNames(int opCode) {
+        return Legacy413Fixtures.journalEntryNames(ROOT, opCode);
+    }
+
+    /**
+     * One of 4.1.5's own GSON label registries, read out of the generated 
{@code labels.*.txt}.
+     *
+     * @param which one of {@code ds}, {@code db}, {@code tbl}
+     */
+    public static Map<String, LegacyLabel> legacyLabels(String which) throws 
IOException {
+        return Legacy413Fixtures.legacyLabels(ROOT, which);
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy415LanceUpgradeTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy415LanceUpgradeTest.java
new file mode 100644
index 00000000000..096104b1dac
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/Legacy415LanceUpgradeTest.java
@@ -0,0 +1,227 @@
+// 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;
+
+import org.apache.doris.common.Pair;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.connector.ConnectorFactory;
+import org.apache.doris.connector.ConnectorPluginManager;
+import org.apache.doris.datasource.log.CatalogLog;
+import org.apache.doris.datasource.log.InitCatalogLog;
+import org.apache.doris.datasource.plugin.PluginDrivenExternalCatalog;
+import org.apache.doris.datasource.upgrade.RecordingConnectorProvider;
+import org.apache.doris.persist.OperationType;
+import org.apache.doris.persist.gson.GsonUtils;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Upgrading a 4.1.4+ cluster that has Lance catalogs to this branch.
+ *
+ * <p>4.1.4 shipped Lance as fe-core classes ({@code LanceExternalCatalog} and 
friends); this branch has no such
+ * classes and, until a Lance connector plugin lands, no provider for the 
"lance" type either. The fixtures were
+ * emitted by real 4.1.5 bytecode 
(src/test/resources/upgrade/415/PROVENANCE.txt). What must hold: the image
+ * loads and the FE starts, the catalog keeps everything the user configured, 
it fails only when used while the
+ * plugin is missing, and a plugin that claims "lance" later is handed exactly 
the persisted properties.
+ *
+ * <p>This class lives in {@code org.apache.doris.datasource} because {@link 
ExternalCatalog#logType} and
+ * {@link ExternalCatalog#tableAutoAnalyzePolicy} are protected.
+ */
+public class Legacy415LanceUpgradeTest {
+
+    private static final long[] LANCE_IDS = {10101L, 10102L, 10103L};
+
+    @AfterEach
+    public void tearDown() {
+        // The plugin manager is a process-wide static shared with every other 
test class in this fork.
+        ConnectorFactory.initPluginManager(new ConnectorPluginManager());
+    }
+
+    @Test
+    public void everyLanceCatalogInThe415ImageLoadsAsPluginDriven() throws 
Exception {
+        // MUTATION: without the LanceExternalCatalog compatible subtype the 
whole module fails to parse
+        // ("cannot deserialize ... subtype named LanceExternalCatalog"), i.e. 
the upgraded FE cannot start.
+        CatalogMgr mgr = Legacy415Fixtures.loadCatalogMgr();
+
+        Assertions.assertEquals(4, mgr.getCatalogNum(),
+                "the 4.1.5 fixture holds the internal catalog plus 3 Lance 
catalogs; a different count means the "
+                        + "fixture was regenerated without updating this 
test");
+        Assertions.assertSame(InternalCatalog.class, 
mgr.getCatalog(0L).getClass());
+        for (long id : LANCE_IDS) {
+            ExternalCatalog catalog = (ExternalCatalog) mgr.getCatalog(id);
+            Assertions.assertSame(PluginDrivenExternalCatalog.class, 
catalog.getClass(),
+                    "catalog " + id + " must be served by the connector 
framework, but loaded as "
+                            + catalog.getClass().getSimpleName());
+            Assertions.assertEquals("lance", catalog.getType());
+            Assertions.assertEquals(InitCatalogLog.Type.PLUGIN, 
catalog.logType,
+                    "a migrated catalog must report the PLUGIN logType, 
otherwise buildDbForInit takes the "
+                            + "legacy path");
+        }
+    }
+
+    @Test
+    public void everyPropertyAndUserVisibleStateSurvivesTheMigration() throws 
Exception {
+        CatalogMgr mgr = Legacy415Fixtures.loadCatalogMgr();
+
+        for (long id : LANCE_IDS) {
+            ExternalCatalog catalog = (ExternalCatalog) mgr.getCatalog(id);
+            Map<String, String> loaded = 
catalog.getCatalogProperty().getProperties();
+            persistedProperties(id).forEach((key, value) -> 
Assertions.assertEquals(value, loaded.get(key),
+                    "property '" + key + "' of catalog " + id + " did not 
survive loading the 4.1.5 image"));
+        }
+
+        ExternalCatalog s3 = (ExternalCatalog) mgr.getCatalog(10101L);
+        Assertions.assertEquals("g1_lance_s3", s3.getName());
+        Assertions.assertEquals("lance on s3", s3.getComment(), "the user's 
COMMENT must survive the upgrade");
+        // Before anything initialises the catalog: makeSureInitialized() 
overwrites lastUpdateTime.
+        Assertions.assertEquals(Legacy415Fixtures.FIXED_UPDATE_TIME, 
s3.getLastUpdateTime());
+        Map<Pair<String, String>, String> policy = s3.tableAutoAnalyzePolicy;
+        Assertions.assertEquals(1, policy.size(), "auto-analyze policies must 
survive the upgrade");
+        Assertions.assertEquals("enable", policy.get(Pair.of("db1", "tbl1")));
+    }
+
+    @Test
+    public void everyLogTypeNameThe415ImageCarriesStillDeserializesToItself() 
throws Exception {
+        // Gson reads an enum name it does not know as null instead of 
failing, so losing LANCE would not break
+        // loading: a Lance catalog always persists its "type" property and 
nothing is backfilled from the
+        // logType. This pins the rule InitCatalogLog.Type documents -- every 
name a released image carries
+        // stays -- for the name 4.1.4 added.
+        Set<String> names = new HashSet<>();
+        for (Map.Entry<String, JsonElement> entry : 
idToCatalogJson().entrySet()) {
+            JsonElement logType = 
entry.getValue().getAsJsonObject().get("logType");
+            if (logType != null) {
+                names.add(logType.getAsString());
+            }
+        }
+        Assertions.assertEquals(java.util.Collections.singleton("LANCE"), 
names,
+                "the fixture's premise changed; regenerate the expectations 
with the fixture");
+        for (String name : names) {
+            InitCatalogLog.Type type = GsonUtils.GSON.fromJson("\"" + name + 
"\"", InitCatalogLog.Type.class);
+            Assertions.assertNotNull(type, "logType '" + name + "' written by 
4.1.5 is no longer a known name");
+            Assertions.assertEquals(name, type.name());
+        }
+    }
+
+    @Test
+    public void lanceCatalogWithoutItsPluginFailsOnlyWhenUsed() throws 
Exception {
+        // The state right after upgrading to a build that ships no Lance 
connector: the image loaded (above),
+        // and touching the catalog reports what is missing instead of 
something obscure.
+        ConnectorFactory.initPluginManager(new ConnectorPluginManager());
+        ExternalCatalog catalog = (ExternalCatalog) 
Legacy415Fixtures.loadCatalogMgr().getCatalog(10102L);
+
+        RuntimeException e = Assertions.assertThrows(RuntimeException.class, 
catalog::makeSureInitialized);
+        Assertions.assertTrue(Throwables.getStackTraceAsString(e).contains(
+                        "No ConnectorProvider found for plugin-driven catalog: 
g1_lance_local, type: lance"),
+                Throwables.getStackTraceAsString(e));
+    }
+
+    @Test
+    public void lanceConnectorIsHandedExactlyThePersistedProperties() throws 
Exception {
+        // Once a plugin claims "lance", the migrated catalog must reach it 
with nothing dropped or renamed --
+        // credentials and namespace options included -- or it loads fine and 
then cannot connect to anything.
+        RecordingConnectorProvider.Registry registry =
+                
RecordingConnectorProvider.installFor(ImmutableList.of("lance"));
+        CatalogMgr mgr = Legacy415Fixtures.loadCatalogMgr();
+
+        for (long id : LANCE_IDS) {
+            ExternalCatalog catalog = (ExternalCatalog) mgr.getCatalog(id);
+            catalog.makeSureInitialized();
+
+            RecordingConnectorProvider lance = registry.get("lance");
+            Assertions.assertEquals(catalog.getName(), 
lance.calls.get(lance.calls.size() - 1).catalogName);
+            Map<String, String> delivered = lance.lastProperties();
+            persistedProperties(id).forEach((key, value) -> 
Assertions.assertEquals(value, delivered.get(key),
+                    "property '" + key + "' of catalog " + id + " did not 
reach the connector unchanged"));
+        }
+    }
+
+    @Test
+    public void everyCreateLanceCatalogEntryFrom415ReplaysWithoutThePlugin() 
throws Exception {
+        // OP_CREATE_CATALOG journals only the property map, so this path 
never depended on the class label; it
+        // is pinned because replay builds the connector synchronously on the 
FE's startup thread, where
+        // EditLog turns any exception into System.exit(-1). Without the 
plugin the catalog registers degraded.
+        ConnectorFactory.initPluginManager(new ConnectorPluginManager());
+
+        for (String fixture : 
Legacy415Fixtures.journalEntryNames(OperationType.OP_CREATE_CATALOG)) {
+            CatalogLog log = readCatalogLog(fixture);
+            CatalogIf<?> catalog = CatalogFactory.createFromLog(log);
+
+            Assertions.assertSame(PluginDrivenExternalCatalog.class, 
catalog.getClass(),
+                    "replay of " + fixture + " produced " + 
catalog.getClass().getSimpleName());
+            Assertions.assertEquals(log.getCatalogName(), catalog.getName());
+            Assertions.assertEquals("lance", ((ExternalCatalog) 
catalog).getType());
+        }
+    }
+
+    @Test
+    public void 
everyCreateLanceCatalogEntryFrom415ReplaysThroughALancePlugin() throws 
Exception {
+        RecordingConnectorProvider.Registry registry =
+                
RecordingConnectorProvider.installFor(ImmutableList.of("lance"));
+
+        for (String fixture : 
Legacy415Fixtures.journalEntryNames(OperationType.OP_CREATE_CATALOG)) {
+            CatalogLog log = readCatalogLog(fixture);
+            CatalogIf<?> catalog = CatalogFactory.createFromLog(log);
+
+            Assertions.assertSame(PluginDrivenExternalCatalog.class, 
catalog.getClass());
+            RecordingConnectorProvider lance = registry.get("lance");
+            Assertions.assertEquals(log.getCatalogName(), 
lance.calls.get(lance.calls.size() - 1).catalogName,
+                    "the provider claiming 'lance' must be asked to build the 
connector for " + fixture);
+            Map<String, String> delivered = lance.lastProperties();
+            log.getProps().forEach((key, value) -> 
Assertions.assertEquals(value, delivered.get(key),
+                    "property '" + key + "' did not survive the journey from 
the 4.1.5 journal to the connector"));
+        }
+    }
+
+    /** The property map catalog {@code id} has on the wire, read straight out 
of the 4.1.5 module's JSON. */
+    private static Map<String, String> persistedProperties(long id) throws 
IOException {
+        JsonObject properties = 
idToCatalogJson().getAsJsonObject(String.valueOf(id))
+                
.getAsJsonObject("catalogProperty").getAsJsonObject("properties");
+        Map<String, String> result = new TreeMap<>();
+        properties.entrySet().forEach(e -> result.put(e.getKey(), 
e.getValue().getAsString()));
+        Assertions.assertTrue(result.size() > 2, "catalog " + id + " has an 
implausibly small property map");
+        return result;
+    }
+
+    private static JsonObject idToCatalogJson() throws IOException {
+        return 
JsonParser.parseString(Legacy415Fixtures.catalogMgrJson()).getAsJsonObject()
+                .getAsJsonObject("idToCatalog");
+    }
+
+    private static CatalogLog readCatalogLog(String fixture) throws 
IOException {
+        byte[] bytes = Legacy415Fixtures.journalEntry(fixture);
+        try (DataInputStream in = new DataInputStream(new 
ByteArrayInputStream(bytes))) {
+            in.readShort(); // op code; the payload class is not on the wire
+            return GsonUtils.GSON.fromJson(Text.readString(in), 
CatalogLog.class);
+        }
+    }
+}
diff --git a/fe/fe-core/src/test/resources/upgrade/415/PROVENANCE.txt 
b/fe/fe-core/src/test/resources/upgrade/415/PROVENANCE.txt
new file mode 100644
index 00000000000..41d17d4eb1c
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/PROVENANCE.txt
@@ -0,0 +1,56 @@
+Golden FE metadata fixtures for upgrading a 4.1.4+ cluster that has Lance 
catalogs.
+
+WHAT THIS IS
+============
+Every .bin / .json / .txt / .index file in this directory except this one was 
produced by REAL Apache Doris
+4.1.5 bytecode, the same way the 4.1.3 fixtures in ../413 were (read 
../413/PROVENANCE.txt for why
+hand-written fixtures are not good enough). 4.1.4 added the Lance catalog; 
4.1.4, 4.1.4.1, 4.1.5 and
+4.2.0-rc01 register the same GSON labels, and the only ones 4.1.3 did not have 
are LanceExternalCatalog,
+LanceExternalDatabase and LanceExternalTable. 4.1.5 is the newest release at 
the time of writing.
+
+PROVENANCE
+==========
+  source tag        : 4.1.5-release  (annotated tag; commit 
917b20be83878e98f94e3ebf7efccc80346e3e50)
+  gson              : 2.10.1 (identical on 4.1.5 and on the branch under test)
+  generator         : generator/Gen415Fixtures.java (committed here verbatim)
+  generated on      : 2026-10-24
+  sha256 datasource.module.bin : 
7a435e135f73d1f2f9b9fa099dc6756e6b01529bf660ced736d923724ebb425d
+
+HOW TO REGENERATE
+=================
+  mkdir -p /path/to/doris-415 && git archive 4.1.5-release | tar -x -C 
/path/to/doris-415
+  cd /path/to/doris-415
+  DORIS_HOME=$PWD DORIS_THIRDPARTY=/path/to/doris/thirdparty bash 
generated-source.sh
+  # compile only, and resolve the classpath inside the same reactor so the 
4.1.5 modules come from their
+  # own target/classes rather than from ~/.m2 (where 1.2-SNAPSHOT is the 
CURRENT branch's):
+  mvn -o -f fe/pom.xml -pl fe-core -am -Dmaven.test.skip=true 
-Dcheckstyle.skip=true -Dlicense.skip=true \
+      -Drat.skip=true -Dmaven.build.cache.enabled=false 
-Dmdep.includeScope=runtime \
+      compile dependency:build-classpath > build.log
+  # the last "Dependencies classpath:" block in build.log is fe-core's; 
prepend fe-core's own classes:
+  CP=$PWD/fe/fe-core/target/classes:$(grep -A1 'Dependencies classpath:' 
build.log | tail -1)
+  javac -d gen-classes -cp "$CP" Gen415Fixtures.java
+  java -cp "gen-classes:$CP" org.apache.doris.tools.Gen415Fixtures <outDir>
+
+  Output is byte-deterministic across JVM runs (verified: two runs produce 
identical files).
+
+  !! NEVER run `mvn install` in the 4.1.5 tree: both trees resolve 
${revision}=1.2-SNAPSHOT.
+
+CONTENTS
+========
+  datasource.module.bin   the image's "datasource" module, framed exactly as 
MetaWriter writes it: the
+                          internal catalog plus three Lance catalogs (ids 
10101-10103), each built through
+                          4.1.5's own CatalogFactory.createFromLog, i.e. what 
a catalog created on 4.1.4+
+                          looks like. Their props always carry "type": 4.1.5 
has no Lance resource type, so
+                          there is no resource-backed (4.1.3 "G2") Lance shape.
+                            10101 g1_lance_s3     filesystem namespace on 
S3-compatible storage, comment, taap
+                            10102 g1_lance_local  filesystem namespace on 
file://
+                            10103 g1_lance_rest   REST namespace with bearer 
auth and namespace options
+  catalogMgr.json         the same JSON unframed, for human diffing. Not read 
by the tests.
+  labels.{ds,db,tbl}.txt  4.1.5's own GSON label registries, read reflectively 
out of GsonUtils.
+  editlog/op320-*.bin     OP_CREATE_CATALOG for each Lance catalog, in 
JournalEntity wire form.
+
+NOT HERE, ON PURPOSE
+====================
+  Lance databases / tables  4.1.5 never persists an external database or table 
(only CatalogMgr.idToCatalog
+                            is serialized), so no image or journal can carry 
those two labels; they are
+                            covered by labels.{db,tbl}.txt.
diff --git a/fe/fe-core/src/test/resources/upgrade/415/catalogMgr.json 
b/fe/fe-core/src/test/resources/upgrade/415/catalogMgr.json
new file mode 100644
index 00000000000..e0e0b3006f3
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/catalogMgr.json
@@ -0,0 +1 @@
+{"idToCatalog":{"0":{"clazz":"InternalCatalog"},"10101":{"clazz":"LanceExternalCatalog","id":10101,"name":"g1_lance_s3","logType":"LANCE","catalogProperty":{"properties":{"type":"lance","lance.catalog.type":"filesystem","warehouse":"s3://warehouse/lance","s3.endpoint":"http://minio:9000","s3.access_key":"ak","s3.secret_key":"sk","s3.region":"us-east-1","use_path_style":"true","use_meta_cache":"true"}},"initialized":false,"lastUpdateTime":1753000000000,"taap":[[{"first":"db1","second":"tb
 [...]
\ No newline at end of file
diff --git a/fe/fe-core/src/test/resources/upgrade/415/datasource.module.bin 
b/fe/fe-core/src/test/resources/upgrade/415/datasource.module.bin
new file mode 100644
index 00000000000..950442861bd
Binary files /dev/null and 
b/fe/fe-core/src/test/resources/upgrade/415/datasource.module.bin differ
diff --git a/fe/fe-core/src/test/resources/upgrade/415/editlog.index 
b/fe/fe-core/src/test/resources/upgrade/415/editlog.index
new file mode 100644
index 00000000000..108483b6afe
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/editlog.index
@@ -0,0 +1,4 @@
+# generated by Gen415Fixtures -- do not edit by hand
+op320-create-g1_lance_local.bin
+op320-create-g1_lance_rest.bin
+op320-create-g1_lance_s3.bin
diff --git 
a/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_local.bin
 
b/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_local.bin
new file mode 100644
index 00000000000..8b3e9d6e15e
Binary files /dev/null and 
b/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_local.bin
 differ
diff --git 
a/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_rest.bin
 
b/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_rest.bin
new file mode 100644
index 00000000000..cb7a287ec9a
Binary files /dev/null and 
b/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_rest.bin
 differ
diff --git 
a/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_s3.bin
 
b/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_s3.bin
new file mode 100644
index 00000000000..02cc6b32221
Binary files /dev/null and 
b/fe/fe-core/src/test/resources/upgrade/415/editlog/op320-create-g1_lance_s3.bin
 differ
diff --git 
a/fe/fe-core/src/test/resources/upgrade/415/generator/Gen415Fixtures.java 
b/fe/fe-core/src/test/resources/upgrade/415/generator/Gen415Fixtures.java
new file mode 100644
index 00000000000..3345ca944ca
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/generator/Gen415Fixtures.java
@@ -0,0 +1,248 @@
+// 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.tools;
+
+import org.apache.doris.common.FeConstants;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.datasource.CatalogFactory;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.CatalogLog;
+import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.datasource.ExternalCatalog;
+import org.apache.doris.persist.gson.GsonUtils;
+import org.apache.doris.persist.gson.RuntimeTypeAdapterFactory;
+
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Generates golden metadata fixtures EXACTLY as Doris 4.1.5 writes them, for 
the Lance catalogs that
+ * 4.1.4 introduced.
+ *
+ * <p>This class is NOT part of Doris. It runs only against a throwaway tree 
checked out at the 4.1.5-release
+ * tag, so that the fixtures it emits are produced by real 4.1.5 bytecode 
rather than by someone's idea of
+ * what 4.1.5 writes. The 4.1.3 fixtures next to these (upgrade/413) were made 
the same way.</p>
+ *
+ * <p>Usage: {@code java -cp <fe-core classes + deps> 
org.apache.doris.tools.Gen415Fixtures <outDir>}</p>
+ */
+public class Gen415Fixtures {
+
+    /** Fixed so the output is byte-deterministic across runs; the same value 
the 4.1.3 generator stamps. */
+    private static final long FIXED_UPDATE_TIME = 1753000000000L;
+
+    private final List<CatalogIf<?>> catalogs = new ArrayList<>();
+    private final List<String> failures = new ArrayList<>();
+
+    public static void main(String[] args) throws Exception {
+        if (args.length < 1) {
+            throw new IllegalArgumentException("usage: Gen415Fixtures 
<outDir>");
+        }
+        FeConstants.runningUnitTest = true;
+        new Gen415Fixtures().run(new File(args[0]));
+    }
+
+    private void run(File outDir) throws Exception {
+        if (!outDir.exists() && !outDir.mkdirs()) {
+            throw new IllegalStateException("cannot create " + outDir);
+        }
+        buildLance();
+
+        CatalogMgr mgr = newCatalogMgrWith(catalogs);
+        String json = GsonUtils.GSON.toJson(mgr);
+
+        writeFramed(new File(outDir, "datasource.module.bin"), json);
+        Files.write(Paths.get(new File(outDir, "catalogMgr.json").toURI()), 
json.getBytes(StandardCharsets.UTF_8));
+        dumpLabels(outDir);
+        buildEditLogFixtures(new File(outDir, "editlog"));
+
+        System.out.println("=== catalogs written: " + catalogs.size());
+        for (CatalogIf<?> c : catalogs) {
+            System.out.println("    " + c.getId() + "  " + 
c.getClass().getSimpleName() + "  " + c.getName());
+        }
+        System.out.println("=== module sha256: " + 
sha256(json.getBytes(StandardCharsets.UTF_8)));
+        if (!failures.isEmpty()) {
+            System.out.println("=== FAILURES (" + failures.size() + "):");
+            failures.forEach(f -> System.out.println("    " + f));
+            throw new IllegalStateException(failures.size() + " fixture(s) 
could not be built");
+        }
+    }
+
+    // 
---------------------------------------------------------------------------------------------------
+    // Lance catalogs, built through 4.1.5's own CatalogFactory replay path, 
i.e. what a catalog CREATED ON
+    // 4.1.4+ looks like. Their props always carry "type": 
Resource.ResourceType has no LANCE value, so a
+    // resource-backed Lance catalog cannot exist and there is no 4.1.3-style 
G2 family to add here.
+    // 
---------------------------------------------------------------------------------------------------
+    private void buildLance() {
+        // The shape of the external_table_p0/lance suites: a filesystem 
namespace on S3-compatible storage.
+        lance(10101, "g1_lance_s3", "lance on s3", props("type", "lance",
+                "lance.catalog.type", "filesystem", "warehouse", 
"s3://warehouse/lance",
+                "s3.endpoint", "http://minio:9000";, "s3.access_key", "ak", 
"s3.secret_key", "sk",
+                "s3.region", "us-east-1", "use_path_style", "true"), true);
+        lance(10102, "g1_lance_local", "", props("type", "lance",
+                "lance.catalog.type", "filesystem", "warehouse", 
"file:///tmp/lance_wh"), false);
+        lance(10103, "g1_lance_rest", "", props("type", "lance",
+                "lance.catalog.type", "rest", "lance.rest.uri", 
"http://lance-rest:2333";,
+                "lance.rest.security.type", "bearer", 
"lance.rest.bearer-token", "tok",
+                "lance.namespace.delimiter", ".", 
"lance.namespace.root_database", "default"), false);
+    }
+
+    private void lance(long id, String name, String comment, Map<String, 
String> props, boolean withTaap) {
+        try {
+            CatalogLog log = new CatalogLog();
+            log.setCatalogId(id);
+            log.setCatalogName(name);
+            log.setComment(comment);
+            log.setProps(props);
+            CatalogIf<?> c = CatalogFactory.createFromLog(log);
+            decorate(c, withTaap);
+            catalogs.add(c);
+        } catch (Throwable t) {
+            failures.add(name + ": " + t);
+        }
+    }
+
+    private void decorate(CatalogIf<?> c, boolean withTaap) {
+        ExternalCatalog ec = (ExternalCatalog) c;
+        ec.setLastUpdateTime(FIXED_UPDATE_TIME);
+        if (withTaap) {
+            // Empty map serializes as {}, non-empty as an array of [key, 
value] 2-tuples
+            // (enableComplexMapKeySerialization); keep one catalog on each 
encoding.
+            ec.setAutoAnalyzePolicy("db1", "tbl1", "enable");
+        }
+    }
+
+    // 
---------------------------------------------------------------------------------------------------
+    // Edit-log fixtures: JournalEntity wire form is writeShort(opCode) + 
payload.write(out), and
+    // CatalogLog.write is Text.writeString(out, GSON.toJson(this)). 
OP_CREATE_CATALOG only; the other
+    // catalog ops carry no catalog type and are covered by the 4.1.3 fixtures.
+    // 
---------------------------------------------------------------------------------------------------
+    private void buildEditLogFixtures(File dir) throws Exception {
+        if (!dir.exists() && !dir.mkdirs()) {
+            throw new IllegalStateException("cannot create " + dir);
+        }
+        for (CatalogIf<?> c : catalogs) {
+            writeOp(dir, 320, "create-" + c.getName(), c.constructEditLog());
+        }
+
+        String[] names = dir.list();
+        if (names == null) {
+            throw new IllegalStateException("cannot list " + dir);
+        }
+        java.util.Arrays.sort(names);
+        StringBuilder sb = new StringBuilder("# generated by Gen415Fixtures -- 
do not edit by hand\n");
+        for (String n : names) {
+            sb.append(n).append('\n');
+        }
+        Files.write(Paths.get(new File(dir.getParentFile(), 
"editlog.index").toURI()),
+                sb.toString().getBytes(StandardCharsets.UTF_8));
+    }
+
+    private void writeOp(File dir, int opCode, String name, CatalogLog log) 
throws Exception {
+        File f = new File(dir, String.format("op%d-%s.bin", opCode, name));
+        try (DataOutputStream out = new DataOutputStream(new 
FileOutputStream(f))) {
+            out.writeShort(opCode);
+            log.write(out);
+        }
+    }
+
+    // 
---------------------------------------------------------------------------------------------------
+    // Label inventory, read reflectively out of 4.1.5's own registries (a 
regex scrape drops labels whose
+    // class name contains a digit).
+    // 
---------------------------------------------------------------------------------------------------
+    private void dumpLabels(File outDir) throws Exception {
+        dumpOneRegistry(outDir, "labels.ds.txt", "dsTypeAdapterFactory");
+        dumpOneRegistry(outDir, "labels.db.txt", "dbTypeAdapterFactory");
+        dumpOneRegistry(outDir, "labels.tbl.txt", "tblTypeAdapterFactory");
+    }
+
+    private void dumpOneRegistry(File outDir, String fileName, String 
fieldName) throws Exception {
+        Field f = GsonUtils.class.getDeclaredField(fieldName);
+        f.setAccessible(true);
+        RuntimeTypeAdapterFactory<?> factory = (RuntimeTypeAdapterFactory<?>) 
f.get(null);
+        Field l = 
RuntimeTypeAdapterFactory.class.getDeclaredField("labelToSubtype");
+        l.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        Map<String, Class<?>> labelToSubtype = (Map<String, Class<?>>) 
l.get(factory);
+        StringBuilder sb = new StringBuilder();
+        sb.append("# generated from 4.1.5 by Gen415Fixtures -- do not edit by 
hand\n");
+        sb.append("# <label>\\t<target class FQN>\\t<ABSTRACT if the label can 
never be emitted>\n");
+        new TreeMap<>(labelToSubtype).forEach((label, clazz) -> 
sb.append(label).append('\t')
+                .append(clazz.getName())
+                
.append(java.lang.reflect.Modifier.isAbstract(clazz.getModifiers()) ? 
"\tABSTRACT" : "")
+                .append('\n'));
+        Files.write(Paths.get(new File(outDir, fileName).toURI()), 
sb.toString().getBytes(StandardCharsets.UTF_8));
+    }
+
+    // 
---------------------------------------------------------------------------------------------------
+    // helpers (same as the 4.1.3 generator)
+    // 
---------------------------------------------------------------------------------------------------
+
+    /**
+     * A CatalogMgr holding the internal catalog (added by its own ctor) plus 
the given externals, injected
+     * straight into idToCatalog: addCatalog() needs a live Env. Only 
idToCatalog is @SerializedName.
+     */
+    private CatalogMgr newCatalogMgrWith(List<CatalogIf<?>> externals) throws 
Exception {
+        CatalogMgr mgr = new CatalogMgr();
+        Field f = CatalogMgr.class.getDeclaredField("idToCatalog");
+        f.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        ConcurrentMap<Long, CatalogIf<?>> idToCatalog = (ConcurrentMap<Long, 
CatalogIf<?>>) f.get(mgr);
+        for (CatalogIf<?> c : externals) {
+            idToCatalog.put(c.getId(), c);
+        }
+        return mgr;
+    }
+
+    private static void writeFramed(File file, String json) throws Exception {
+        try (DataOutputStream out = new DataOutputStream(new 
FileOutputStream(file))) {
+            Text.writeString(out, json);
+        }
+    }
+
+    private static Map<String, String> props(String... kv) {
+        if (kv.length % 2 != 0) {
+            throw new IllegalArgumentException("odd number of props");
+        }
+        Map<String, String> m = new LinkedHashMap<>();
+        for (int i = 0; i < kv.length; i += 2) {
+            m.put(kv[i], kv[i + 1]);
+        }
+        return m;
+    }
+
+    private static String sha256(byte[] bytes) throws Exception {
+        MessageDigest md = MessageDigest.getInstance("SHA-256");
+        StringBuilder sb = new StringBuilder();
+        for (byte b : md.digest(bytes)) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
+    }
+}
diff --git a/fe/fe-core/src/test/resources/upgrade/415/labels.db.txt 
b/fe/fe-core/src/test/resources/upgrade/415/labels.db.txt
new file mode 100644
index 00000000000..7140ed51acd
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/labels.db.txt
@@ -0,0 +1,15 @@
+# generated from 4.1.5 by Gen415Fixtures -- do not edit by hand
+# <label>\t<target class FQN>\t<ABSTRACT if the label can never be emitted>
+EsExternalDatabase     org.apache.doris.datasource.es.EsExternalDatabase
+ExternalDatabase       org.apache.doris.datasource.ExternalDatabase    ABSTRACT
+ExternalInfoSchemaDatabase     
org.apache.doris.datasource.infoschema.ExternalInfoSchemaDatabase
+ExternalMysqlDatabase  
org.apache.doris.datasource.infoschema.ExternalMysqlDatabase
+HMSExternalDatabase    org.apache.doris.datasource.hive.HMSExternalDatabase
+IcebergExternalDatabase        
org.apache.doris.datasource.iceberg.IcebergExternalDatabase
+JdbcExternalDatabase   org.apache.doris.datasource.jdbc.JdbcExternalDatabase
+LakeSoulExternalDatabase       
org.apache.doris.datasource.lakesoul.LakeSoulExternalDatabase
+LanceExternalDatabase  org.apache.doris.datasource.lance.LanceExternalDatabase
+MaxComputeExternalDatabase     
org.apache.doris.datasource.maxcompute.MaxComputeExternalDatabase
+PaimonExternalDatabase 
org.apache.doris.datasource.paimon.PaimonExternalDatabase
+TestExternalDatabase   org.apache.doris.datasource.test.TestExternalDatabase
+TrinoConnectorExternalDatabase 
org.apache.doris.datasource.trinoconnector.TrinoConnectorExternalDatabase
diff --git a/fe/fe-core/src/test/resources/upgrade/415/labels.ds.txt 
b/fe/fe-core/src/test/resources/upgrade/415/labels.ds.txt
new file mode 100644
index 00000000000..6fef4b8a617
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/labels.ds.txt
@@ -0,0 +1,26 @@
+# generated from 4.1.5 by Gen415Fixtures -- do not edit by hand
+# <label>\t<target class FQN>\t<ABSTRACT if the label can never be emitted>
+CloudInternalCatalog   org.apache.doris.cloud.datasource.CloudInternalCatalog
+EsExternalCatalog      org.apache.doris.datasource.es.EsExternalCatalog
+HMSExternalCatalog     org.apache.doris.datasource.hive.HMSExternalCatalog
+IcebergDLFExternalCatalog      
org.apache.doris.datasource.iceberg.IcebergDLFExternalCatalog
+IcebergExternalCatalog 
org.apache.doris.datasource.iceberg.IcebergExternalCatalog      ABSTRACT
+IcebergGlueExternalCatalog     
org.apache.doris.datasource.iceberg.IcebergGlueExternalCatalog
+IcebergHMSExternalCatalog      
org.apache.doris.datasource.iceberg.IcebergHMSExternalCatalog
+IcebergHadoopExternalCatalog   
org.apache.doris.datasource.iceberg.IcebergHadoopExternalCatalog
+IcebergJdbcExternalCatalog     
org.apache.doris.datasource.iceberg.IcebergJdbcExternalCatalog
+IcebergRestExternalCatalog     
org.apache.doris.datasource.iceberg.IcebergRestExternalCatalog
+IcebergS3TablesExternalCatalog 
org.apache.doris.datasource.iceberg.IcebergS3TablesExternalCatalog
+InternalCatalog        org.apache.doris.datasource.InternalCatalog
+JdbcExternalCatalog    org.apache.doris.datasource.jdbc.JdbcExternalCatalog
+LakeSoulExternalCatalog        
org.apache.doris.datasource.lakesoul.LakeSoulExternalCatalog
+LanceExternalCatalog   org.apache.doris.datasource.lance.LanceExternalCatalog
+MaxComputeExternalCatalog      
org.apache.doris.datasource.maxcompute.MaxComputeExternalCatalog
+PaimonDLFExternalCatalog       
org.apache.doris.datasource.paimon.PaimonDLFExternalCatalog
+PaimonExternalCatalog  org.apache.doris.datasource.paimon.PaimonExternalCatalog
+PaimonFileExternalCatalog      
org.apache.doris.datasource.paimon.PaimonFileExternalCatalog
+PaimonHMSExternalCatalog       
org.apache.doris.datasource.paimon.PaimonHMSExternalCatalog
+PaimonRestExternalCatalog      
org.apache.doris.datasource.paimon.PaimonRestExternalCatalog
+RemoteDorisExternalCatalog     
org.apache.doris.datasource.doris.RemoteDorisExternalCatalog
+TestExternalCatalog    org.apache.doris.datasource.test.TestExternalCatalog
+TrinoConnectorExternalCatalog  
org.apache.doris.datasource.trinoconnector.TrinoConnectorExternalCatalog
diff --git a/fe/fe-core/src/test/resources/upgrade/415/labels.tbl.txt 
b/fe/fe-core/src/test/resources/upgrade/415/labels.tbl.txt
new file mode 100644
index 00000000000..43f20a73d36
--- /dev/null
+++ b/fe/fe-core/src/test/resources/upgrade/415/labels.tbl.txt
@@ -0,0 +1,29 @@
+# generated from 4.1.5 by Gen415Fixtures -- do not edit by hand
+# <label>\t<target class FQN>\t<ABSTRACT if the label can never be emitted>
+BrokerTable    org.apache.doris.catalog.BrokerTable
+Dictionary     org.apache.doris.dictionary.Dictionary
+EsExternalTable        org.apache.doris.datasource.es.EsExternalTable
+EsTable        org.apache.doris.catalog.EsTable
+ExternalInfoSchemaTable        
org.apache.doris.datasource.infoschema.ExternalInfoSchemaTable
+ExternalMysqlTable     
org.apache.doris.datasource.infoschema.ExternalMysqlTable
+ExternalTable  org.apache.doris.datasource.ExternalTable
+FunctionGenTable       org.apache.doris.catalog.FunctionGenTable
+HMSExternalTable       org.apache.doris.datasource.hive.HMSExternalTable
+HiveTable      org.apache.doris.catalog.HiveTable
+IcebergExternalTable   org.apache.doris.datasource.iceberg.IcebergExternalTable
+InlineView     org.apache.doris.catalog.InlineView
+JdbcExternalTable      org.apache.doris.datasource.jdbc.JdbcExternalTable
+JdbcTable      org.apache.doris.catalog.JdbcTable
+LakeSoulExternalTable  
org.apache.doris.datasource.lakesoul.LakeSoulExternalTable
+LanceExternalTable     org.apache.doris.datasource.lance.LanceExternalTable
+MTMV   org.apache.doris.catalog.MTMV
+MaxComputeExternalTable        
org.apache.doris.datasource.maxcompute.MaxComputeExternalTable
+MysqlDBTable   org.apache.doris.catalog.MysqlDBTable
+MysqlTable     org.apache.doris.catalog.MysqlTable
+OdbcTable      org.apache.doris.catalog.OdbcTable
+OlapTable      org.apache.doris.catalog.OlapTable
+PaimonExternalTable    org.apache.doris.datasource.paimon.PaimonExternalTable
+SchemaTable    org.apache.doris.catalog.SchemaTable
+TestExternalTable      org.apache.doris.datasource.test.TestExternalTable
+TrinoConnectorExternalTable    
org.apache.doris.datasource.trinoconnector.TrinoConnectorExternalTable
+View   org.apache.doris.catalog.View


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

Reply via email to