[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 <lucian1412@outlook.com>
This commit is contained in:
Mingyu Chen (Rayner)andkid authored and GitHub committed 2026-10-29 13:04:08 +08:00
1 parent 2760a7663b
commit d420f9c41b
20 files changed
+984 -16

No files matched your search

+7
View File
@@ -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"
@@ -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));
@@ -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;
}
@@ -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())
@@ -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);
}
}
@@ -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());
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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.
@@ -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":"tbl1"},"enable"]],"comment":"lance on s3"},"10102":{"clazz":"LanceExternalCatalog","id":10102,"name":"g1_lance_local","logType":"LANCE","catalogProperty":{"properties":{"type":"lance","lance.catalog.type":"filesystem","warehouse":"file:///tmp/lance_wh","use_meta_cache":"true"}},"initialized":false,"lastUpdateTime":1753000000000,"taap":{},"comment":""},"10103":{"clazz":"LanceExternalCatalog","id":10103,"name":"g1_lance_rest","logType":"LANCE","catalogProperty":{"properties":{"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","use_meta_cache":"true"}},"initialized":false,"lastUpdateTime":1753000000000,"taap":{},"comment":""}}}
@@ -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
@@ -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();
}
}
@@ -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
@@ -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
@@ -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