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 fc2037fcdb0 [fix](paimon) Keep chain groups in Paimon fallback
rebuilds and reject chain table scans clearly (#68897)
fc2037fcdb0 is described below
commit fc2037fcdb04063104c40e1700cb9c1e45a1ecc6
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Oct 13 15:02:06 2026 +0800
[fix](paimon) Keep chain groups in Paimon fallback rebuilds and reject
chain table scans clearly (#68897)
### What problem does this PR solve?
Issue Number: None
Related PR: #68885
Problem Summary:
**In short.** Master cannot read a Paimon 1.4 chain table: every data
scan fails in planning with Paimon's internal `This is a bug, perhaps
there is a recursive call.` This PR keeps the chain group intact
wherever the Paimon connector and the BE JNI scanner rebuild a fallback
pair, and makes a data scan of a chain table fail with a clear "not
supported yet" message, because the rest of the scan path does not plan
chain partitions and chain splits yet. The review of #68885 (closed
since) found the BE half of the problem.
**Background.** Paimon 1.4 builds a chain table (`chain-table.enabled`
with `scan.fallback-snapshot-branch` and `scan.fallback-delta-branch`)
as `FallbackReadFileStoreTable(main, ChainGroupReadTable(snapshot,
delta), true)`. `ChainGroupReadTable` is itself a fallback pair. It
plans and reads the chain: a partition missing from the snapshot branch
is read as the latest earlier snapshot partition merged with the delta
branch. Paimon's `FallbackReadFileStoreTable` constructor rejects any
other pair nested inside a pair. The Paimon connector and the BE JNI
scanner rebuild fallback pairs branch by branch in six places:
- the manifest-parallelism bound on the BE (`PaimonJniScanner`, taken
when the FE cap is below the BE's core count) and on the FE
(`PaimonReaderOptions`);
- the catalog-loader strip (`dropCatalogLoader`) and the catalog
snapshot pin (`pinCatalogSnapshot`) of the table sent to the BE;
- the restore of a statement pin: pinned fallback snapshots
(`copyWithPinnedFallback`) and bound schemas (`restoreBoundSchema`).
**The problem, and what it cost.** Each of these recursed into the chain
group and rebuilt it either as a plain pair, which the outer constructor
rejects, or as a plain table of the snapshot branch, which silently
drops the delta branch. On a local FE + BE with a file-system Paimon
catalog and a chain table created and written through the Paimon API:
| Read | Paimon's own read | master |
|---|---|---|
| `SELECT * FROM chain_t` | 5 rows: 2 from the snapshot branch, 3
chained from the snapshot and delta branches | fails: `This is a bug,
perhaps there is a recursive call.` (first from
`PaimonScanParams.restoreBoundSchema`) |
| a `$files` scan when the FE cap is below the BE's core count | | the
BE manifest bound fails the same way (unit test; a single host does not
take that path) |
Keeping the chain group is not enough to read a chain table. Doris plans
neither the partitions a chain table reads from its branches nor the
chain splits. With only the rebuilds fixed, a chain table whose main
branch is empty reads as empty (0 partitions, 0 splits), and one with
main data fails with `Fail to parse columnsFromPath, expected: [dt],
filePath: connector://virtual`. branch-4.x has the same manifest-bound
rebuild and no chain support either.
**How this PR fixes it.**
- `PaimonTableDecorators.rebuildFallbackPair` rebuilds a
`ChainGroupReadTable` as a group and any other pair as before. The FE
manifest bound, `copyWithPinnedFallback` and `restoreBoundSchema` use
it, and `dropCatalogLoader` and `pinCatalogSnapshot` now recurse into
the nested pair. The BE manifest bound keeps the group the same way.
- `PaimonScanPlanProvider.resolveScanTable` rejects a data scan of a
chain table (a fallback pair over a `ChainGroupReadTable`) with `Reading
Paimon chain table db.tbl (chain-table.enabled) is not supported yet`.
System tables and a branch read on its own (`@branch(...)`) are plain
tables and are not affected.
**Results.** Same cluster and table:
| Read | before | after |
|---|---|---|
| `SELECT ... FROM chain_t` | `This is a bug, perhaps there is a
recursive call.` | `Reading Paimon chain table d.chain_t
(chain-table.enabled) is not supported yet` |
| ``chain_t$files`` | reads the main branch's files | same |
| `chain_t@branch(snapshot)`, `chain_t@branch(delta)` | read that
branch's rows | same |
| an ordinary Paimon table | reads | same |
System tables over a chain table now read through the chain group too.
Paimon resolves `$partitions` on the backend, and for a chain table
whose delta branch alone holds partition 20240102 it returns `[20231231,
20240101]` from master's backend payload and `[20231231, 20240101,
20240102]`, Paimon's own answer, from this PR's (Paimon 1.4.2 in jshell
on the same payloads, not on the cluster). `chain_t$ro` is unchanged: it
returns Paimon's own read-optimized rows, which leave out a partition
only the delta branch holds.
---
.../org/apache/doris/paimon/PaimonJniScanner.java | 6 ++
.../apache/doris/paimon/PaimonJniScannerTest.java | 67 +++++++++++++++++++++
.../connector/paimon/PaimonReaderOptions.java | 3 +-
.../doris/connector/paimon/PaimonScanParams.java | 11 ++--
.../connector/paimon/PaimonScanPlanProvider.java | 39 ++++++++-----
.../connector/paimon/PaimonTableDecorators.java | 28 +++++++++
.../paimon/PaimonBackendBoundTableTest.java | 58 ++++++++++++++++++
.../connector/paimon/PaimonReaderOptionsTest.java | 35 +++++++++++
.../paimon/PaimonStatementSchemaTest.java | 68 ++++++++++++++++++++++
9 files changed, 293 insertions(+), 22 deletions(-)
diff --git
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
index 6b349f1556d..c7b410c3102 100644
---
a/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
+++
b/fe/be-java-extensions/paimon-scanner/src/main/java/org/apache/doris/paimon/PaimonJniScanner.java
@@ -32,6 +32,7 @@ import org.apache.paimon.options.MemorySize;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.reader.FileRecordIterator;
import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.table.ChainGroupReadTable;
import org.apache.paimon.table.DelegatedFileStoreTable;
import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.FileStoreTable;
@@ -778,6 +779,11 @@ public class PaimonJniScanner extends JniScanner {
}
// Each branch owns an independent planner setting; a smaller
sibling is not an
// execution ceiling and must never throttle the other branch.
+ if (pair instanceof ChainGroupReadTable) {
+ // A chain table nests this group over its snapshot and delta
branches. The group plans
+ // and reads the chain itself and is the only pair Paimon lets
one nest inside another.
+ return new ChainGroupReadTable(main, other);
+ }
return new FallbackReadFileStoreTable(main, other,
isWrappedFirst(pair));
}
diff --git
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
index 4cdc78dea2e..b4cabd0f424 100644
---
a/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
+++
b/fe/be-java-extensions/paimon-scanner/src/test/java/org/apache/doris/paimon/PaimonJniScannerTest.java
@@ -28,13 +28,16 @@ import org.apache.paimon.disk.BufferFileWriter;
import org.apache.paimon.disk.FileIOChannel;
import org.apache.paimon.disk.IOManager;
import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.privilege.PrivilegeChecker;
import org.apache.paimon.privilege.PrivilegedFileStoreTable;
import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.TableSchema;
+import org.apache.paimon.table.ChainGroupReadTable;
import org.apache.paimon.table.DelegatedFileStoreTable;
import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.FileStoreTableFactory;
import org.apache.paimon.table.Table;
import org.apache.paimon.table.source.ReadBuilder;
import org.apache.paimon.table.source.TableRead;
@@ -315,6 +318,60 @@ public class PaimonJniScannerTest {
.options().get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
}
+ @Test
+ public void testBackendManifestCapKeepsTheChainGroupOfAChainTable(@TempDir
Path dir) {
+ // Paimon builds a chain table as a fallback pair whose other branch
is a ChainGroupReadTable, and the outer
+ // pair accepts no other nested fallback. Bounding the manifest
parallelism rebuilds every branch, so the
+ // chain group has to come back as one. MUTATION: rebuilding it as a
plain fallback pair -> red.
+ Table chain = new FallbackReadFileStoreTable(primaryKeyTable(dir,
"main"),
+ new ChainGroupReadTable(primaryKeyTable(dir, "snapshot"),
primaryKeyTable(dir, "delta")), true);
+
+ Table safe = PaimonJniScanner.applyBackendManifestParallelism(chain,
"2", 4);
+
+ FileStoreTable group = ((FallbackReadFileStoreTable) safe).other();
+ Assertions.assertTrue(group instanceof ChainGroupReadTable,
group.getClass().getName());
+ Assertions.assertEquals("2", ((ChainGroupReadTable)
group).wrapped().options()
+ .get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
+ Assertions.assertEquals("2", ((ChainGroupReadTable)
group).other().options()
+ .get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
+ }
+
+ @Test
+ public void
testBackendCapKeepsTheChainGroupOfAChainTableSystemSource(@TempDir Path dir)
throws Exception {
+ // A system table over a chain table ($partitions, $files, ...) is
rebuilt from the transported source, and
+ // the rebuilt wrapper must still read through the chain group: the
backend resolves $partitions, and
+ // without the delta branch a partition only it holds goes missing.
MUTATION: rebuilding the group as a
+ // plain fallback pair -> red.
+ FileStoreTable chain = new
FallbackReadFileStoreTable(primaryKeyTable(dir, "main"),
+ new ChainGroupReadTable(primaryKeyTable(dir, "snapshot"),
primaryKeyTable(dir, "delta")), true);
+
+ Table safe = PaimonJniScanner.applyBackendManifestParallelism(
+ SystemTableLoader.load("partitions", chain), "2", 4, chain,
"partitions");
+
+ Field storeTable = safe.getClass().getDeclaredField("storeTable");
+ storeTable.setAccessible(true);
+ FileStoreTable group = ((FallbackReadFileStoreTable)
storeTable.get(safe)).other();
+ Assertions.assertTrue(group instanceof ChainGroupReadTable,
group.getClass().getName());
+ Assertions.assertEquals("2", ((ChainGroupReadTable)
group).other().options()
+ .get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
+ }
+
+ @Test
+ public void
testBackendCapStillReadsTheChainTablePayloadOfAnOlderFrontend(@TempDir Path
dir) {
+ // An older FE sends a chain table with its group replaced by a plain
table at the snapshot branch. A backend
+ // upgraded before the FE keeps reading that payload as it did.
MUTATION: rebuilding a pair as a chain group
+ // whatever it holds -> red.
+ FileStoreTable flattened = new
FallbackReadFileStoreTable(primaryKeyTable(dir, "main"),
+ primaryKeyTable(dir, "snapshot"), true);
+
+ Table safe =
PaimonJniScanner.applyBackendManifestParallelism(flattened, "2", 4);
+
+ Assertions.assertEquals(FallbackReadFileStoreTable.class,
safe.getClass());
+ FileStoreTable other = ((FallbackReadFileStoreTable) safe).other();
+ Assertions.assertFalse(other instanceof FallbackReadFileStoreTable,
other.getClass().getName());
+ Assertions.assertEquals("2",
other.options().get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
+ }
+
@Test
public void testAdvertisedFeCapStillChecksSerializedChildren() {
FileStoreTable main =
serializableFileStoreTable(Collections.emptyMap());
@@ -516,6 +573,16 @@ public class PaimonJniScannerTest {
.get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
}
+ /** A primary-key table object, as each branch of a chain table is; no
files are written. */
+ private static FileStoreTable primaryKeyTable(Path dir, String name) {
+ TableSchema schema = new TableSchema(0L,
+ Arrays.asList(new DataField(0, "id",
DataTypes.INT().notNull()),
+ new DataField(1, "v", DataTypes.STRING())),
+ 1, Collections.emptyList(), Collections.singletonList("id"),
Collections.emptyMap(), "");
+ return FileStoreTableFactory.create(LocalFileIO.create(),
+ new
org.apache.paimon.fs.Path(dir.resolve(name).toUri().toString()), schema);
+ }
+
private static FileStoreTable serializableFileStoreTable(Map<String,
String> options) {
return (FileStoreTable)
Proxy.newProxyInstance(FileStoreTable.class.getClassLoader(),
new Class[] {FileStoreTable.class}, new
SerializableTableHandler(options));
diff --git
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonReaderOptions.java
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonReaderOptions.java
index 94b05c8563f..de09d4e9132 100644
---
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonReaderOptions.java
+++
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonReaderOptions.java
@@ -223,8 +223,7 @@ public final class PaimonReaderOptions {
FileStoreTable other = normalizeManifestParallelism(
pair.other(), safeBound, materializeAbsent);
return main == pair.wrapped() && other == pair.other()
- ? table : new FallbackReadFileStoreTable(
- main, other, isWrappedFirst(pair));
+ ? table : PaimonTableDecorators.rebuildFallbackPair(pair,
main, other);
}
if (table instanceof DelegatedFileStoreTable) {
diff --git
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanParams.java
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanParams.java
index 829ebd4cddc..5e4c59f33f7 100644
---
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanParams.java
+++
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanParams.java
@@ -224,10 +224,9 @@ public final class PaimonScanParams {
fallbackOptions.remove(CoreOptions.BUCKET.key());
fallbackOptions.put(CoreOptions.BRANCH.key(),
pair.other().coreOptions().branch());
fallbackOptions.put(CoreOptions.SCAN_SNAPSHOT_ID.key(),
snapshotId);
- return new FallbackReadFileStoreTable(
+ return PaimonTableDecorators.rebuildFallbackPair(pair,
copyWithPinnedFallback(pair.wrapped(), dynamicOptions,
coordinates, path),
- copyWithPinnedFallback(pair.other(), fallbackOptions,
coordinates, fallbackPath),
- PaimonReaderOptions.isWrappedFirst(pair));
+ copyWithPinnedFallback(pair.other(), fallbackOptions,
coordinates, fallbackPath));
}
}
if (table instanceof DelegatedFileStoreTable && !(table instanceof
FallbackReadFileStoreTable)) {
@@ -246,9 +245,9 @@ public final class PaimonScanParams {
String fallbackPath = path + "fallback.";
long fallbackId = PaimonSchemaPin.fallbackSchemaId(options,
fallbackPath,
() ->
pair.other().schemaManager().latest().orElseThrow(IllegalStateException::new).id());
- return new
FallbackReadFileStoreTable(restoreBoundSchema(pair.wrapped(), schemaId,
options, path),
- restoreBoundSchema(pair.other(), fallbackId, options,
fallbackPath),
- PaimonReaderOptions.isWrappedFirst(pair));
+ return PaimonTableDecorators.rebuildFallbackPair(pair,
+ restoreBoundSchema(pair.wrapped(), schemaId, options,
path),
+ restoreBoundSchema(pair.other(), fallbackId, options,
fallbackPath));
}
if (table instanceof DelegatedFileStoreTable) {
FileStoreTable wrapped = ((DelegatedFileStoreTable)
table).wrapped();
diff --git
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java
index 3d912bbebd2..2a2157e1bdc 100644
---
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java
+++
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java
@@ -346,6 +346,12 @@ public class PaimonScanPlanProvider implements
ConnectorScanPlanProvider {
*/
Table resolveScanTable(PaimonTableHandle paimonHandle) {
Table table = resolveTable(paimonHandle);
+ if (!paimonHandle.isSystemTable() &&
PaimonTableDecorators.isChainTable(table)) {
+ // A chain table reads a partition its main branch lacks from its
snapshot and delta branches.
+ // Doris plans neither those partitions nor the chain splits yet,
so it would read them as empty.
+ throw new DorisConnectorException("Reading Paimon chain table " +
paimonHandle.getDatabaseName() + "."
+ + paimonHandle.getTableName() + " (chain-table.enabled) is
not supported yet");
+ }
return withBoundSchemaAuthentication(paimonHandle, () ->
applyScanOptions(paimonHandle, table));
}
@@ -1504,18 +1510,21 @@ public class PaimonScanPlanProvider implements
ConnectorScanPlanProvider {
*/
// Package-private for direct unit testing (PaimonBackendBoundTableTest).
static FileStoreTable pinCatalogSnapshot(FileStoreTable catalogLessTable,
FileStoreTable dataTable) {
- FileStoreTable source =
PaimonTableDecorators.unwrapToFallbackOrBase(dataTable);
- FileStoreTable target =
PaimonTableDecorators.unwrapToFallbackOrBase(catalogLessTable);
+ return
pinCatalogSnapshotOfBranches(PaimonTableDecorators.unwrapToFallbackOrBase(catalogLessTable),
+ PaimonTableDecorators.unwrapToFallbackOrBase(dataTable));
+ }
+
+ private static FileStoreTable pinCatalogSnapshotOfBranches(FileStoreTable
target, FileStoreTable source) {
if (source instanceof FallbackReadFileStoreTable) {
if (!(target instanceof FallbackReadFileStoreTable)) {
throw new IllegalStateException("Catalog-less Paimon table
lost its fallback branch");
}
FallbackReadFileStoreTable sourcePair =
(FallbackReadFileStoreTable) source;
FallbackReadFileStoreTable targetPair =
(FallbackReadFileStoreTable) target;
- return new FallbackReadFileStoreTable(
- pinCatalogSnapshotBranch(targetPair.wrapped(),
sourcePair.wrapped()),
- pinCatalogSnapshotBranch(targetPair.other(),
sourcePair.other()),
- PaimonReaderOptions.isWrappedFirst(targetPair));
+ // A chain table nests its snapshot and delta branches in a pair
of their own; pin those too.
+ return PaimonTableDecorators.rebuildFallbackPair(targetPair,
+ pinCatalogSnapshotOfBranches(targetPair.wrapped(),
sourcePair.wrapped()),
+ pinCatalogSnapshotOfBranches(targetPair.other(),
sourcePair.other()));
}
return pinCatalogSnapshotBranch(target, source);
}
@@ -1553,15 +1562,17 @@ public class PaimonScanPlanProvider implements
ConnectorScanPlanProvider {
if (dataTable.catalogEnvironment().catalogLoader() == null) {
return dataTable;
}
- FileStoreTable undecorated =
PaimonTableDecorators.unwrapToFallbackOrBase(dataTable);
- if (undecorated instanceof FallbackReadFileStoreTable) {
- FallbackReadFileStoreTable fallbackReadTable =
(FallbackReadFileStoreTable) undecorated;
- return new FallbackReadFileStoreTable(
- rebuildWithoutCatalogLoader(fallbackReadTable.wrapped()),
- rebuildWithoutCatalogLoader(fallbackReadTable.other()),
- PaimonReaderOptions.isWrappedFirst(fallbackReadTable));
+ return
dropCatalogLoaderOfBranches(PaimonTableDecorators.unwrapToFallbackOrBase(dataTable));
+ }
+
+ private static FileStoreTable dropCatalogLoaderOfBranches(FileStoreTable
table) {
+ if (table instanceof FallbackReadFileStoreTable) {
+ // A chain table nests its snapshot and delta branches in a pair
of their own; rebuild those too.
+ FallbackReadFileStoreTable pair = (FallbackReadFileStoreTable)
table;
+ return PaimonTableDecorators.rebuildFallbackPair(pair,
+ dropCatalogLoaderOfBranches(pair.wrapped()),
dropCatalogLoaderOfBranches(pair.other()));
}
- return rebuildWithoutCatalogLoader(undecorated);
+ return rebuildWithoutCatalogLoader(table);
}
private static FileStoreTable rebuildWithoutCatalogLoader(FileStoreTable
branch) {
diff --git
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonTableDecorators.java
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonTableDecorators.java
index b762ff35e4f..fd82806e085 100644
---
a/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonTableDecorators.java
+++
b/fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonTableDecorators.java
@@ -20,9 +20,11 @@ package org.apache.doris.connector.paimon;
import org.apache.paimon.catalog.Identifier;
import org.apache.paimon.privilege.PrivilegeChecker;
import org.apache.paimon.privilege.PrivilegedFileStoreTable;
+import org.apache.paimon.table.ChainGroupReadTable;
import org.apache.paimon.table.DelegatedFileStoreTable;
import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.Table;
import java.lang.reflect.Field;
@@ -54,6 +56,32 @@ final class PaimonTableDecorators {
}
}
+ /**
+ * Rebuild a fallback-branch pair around new branches. A chain table is a
pair whose other branch is a
+ * {@link ChainGroupReadTable} over its snapshot and delta branches: the
group plans and reads the chain
+ * itself, and it is the only pair Paimon lets one nest inside another, so
it is rebuilt as a group.
+ */
+ static FileStoreTable rebuildFallbackPair(FallbackReadFileStoreTable pair,
FileStoreTable wrapped,
+ FileStoreTable other) {
+ if (pair instanceof ChainGroupReadTable) {
+ return new ChainGroupReadTable(wrapped, other);
+ }
+ return new FallbackReadFileStoreTable(wrapped, other,
PaimonReaderOptions.isWrappedFirst(pair));
+ }
+
+ /**
+ * Whether {@code table} reads as a chain table: a fallback pair whose
other branch is a
+ * {@link ChainGroupReadTable}. A branch of a chain table read on its own
is a plain table.
+ */
+ static boolean isChainTable(Table table) {
+ if (!(table instanceof FileStoreTable)) {
+ return false;
+ }
+ FileStoreTable base = unwrapToFallbackOrBase((FileStoreTable) table);
+ return base instanceof FallbackReadFileStoreTable
+ && ((FallbackReadFileStoreTable) base).other() instanceof
ChainGroupReadTable;
+ }
+
/**
* Peel the decorators Paimon may have stacked on top of the table, down
to the fallback-branch
* pair - the one layer that must stay on top, because that is what
dispatches a read to the
diff --git
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonBackendBoundTableTest.java
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonBackendBoundTableTest.java
index 03e4703f117..bca0c1544e0 100644
---
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonBackendBoundTableTest.java
+++
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonBackendBoundTableTest.java
@@ -17,6 +17,8 @@
package org.apache.doris.connector.paimon;
+import org.apache.doris.connector.spi.DorisConnectorException;
+
import org.apache.paimon.CoreOptions;
import org.apache.paimon.Snapshot;
import org.apache.paimon.catalog.FileSystemCatalog;
@@ -32,6 +34,7 @@ import org.apache.paimon.schema.SchemaChange;
import org.apache.paimon.schema.SchemaManager;
import org.apache.paimon.schema.TableSchema;
import org.apache.paimon.table.CatalogEnvironment;
+import org.apache.paimon.table.ChainGroupReadTable;
import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.FileStoreTable;
import org.apache.paimon.table.FileStoreTableFactory;
@@ -144,6 +147,51 @@ public class PaimonBackendBoundTableTest {
sqlNull.getStaticPartition().get("pt"));
}
+ @Test
+ public void chainTableKeepsItsChainGroupWithoutTheCatalogLoader(@TempDir
Path warehouse) {
+ // Paimon builds a chain table as a fallback pair whose other branch
is a ChainGroupReadTable over the
+ // snapshot and the delta branch. Every branch is rebuilt without the
loader, so the chain group has to
+ // come back as one, with both of its branches. MUTATION: rebuilding
the chain group as one plain table
+ // -> red.
+ VersionManagedCatalog catalog = new VersionManagedCatalog(warehouse,
false);
+ CatalogEnvironment env = catalogEnvironment(catalog);
+ FileStoreTable snapshot = primaryKeyTable(warehouse, "snapshot", env);
+ FileStoreTable delta = primaryKeyTable(warehouse, "delta", env);
+ FileStoreTable chain = new
FallbackReadFileStoreTable(primaryKeyTable(warehouse, "main", env),
+ new ChainGroupReadTable(snapshot, delta), true);
+
+ FileStoreTable forBackend =
PaimonScanPlanProvider.dropCatalogLoader(chain);
+
+ FileStoreTable group = ((FallbackReadFileStoreTable)
forBackend).other();
+ Assertions.assertTrue(group instanceof ChainGroupReadTable,
group.getClass().getName());
+ ChainGroupReadTable chainGroup = (ChainGroupReadTable) group;
+ Assertions.assertEquals(snapshot.location(),
chainGroup.wrapped().location());
+ Assertions.assertEquals(delta.location(),
chainGroup.other().location());
+
Assertions.assertNull(chainGroup.wrapped().catalogEnvironment().catalogLoader());
+
Assertions.assertNull(chainGroup.other().catalogEnvironment().catalogLoader());
+ }
+
+ @Test
+ public void scanOfAChainTableFailsClearly(@TempDir Path warehouse) {
+ // Doris plans neither the partitions a chain table reads from its
snapshot and delta branches nor the chain
+ // splits, so a scan would read them as empty; it fails instead. A
branch read on its own is a plain table
+ // and still reads. MUTATION: dropping the check -> red.
+ VersionManagedCatalog catalog = new VersionManagedCatalog(warehouse,
false);
+ CatalogEnvironment env = catalogEnvironment(catalog);
+ FileStoreTable snapshot = primaryKeyTable(warehouse, "snapshot", env);
+ FileStoreTable chain = new
FallbackReadFileStoreTable(primaryKeyTable(warehouse, "main", env),
+ new ChainGroupReadTable(snapshot, primaryKeyTable(warehouse,
"delta", env)), true);
+ PaimonTableHandle handle = dataHandle();
+ handle.setPaimonTable(chain);
+ PaimonTableHandle branch = dataHandle();
+ branch.setPaimonTable(snapshot);
+
+ DorisConnectorException error =
Assertions.assertThrows(DorisConnectorException.class,
+ () -> provider().resolveScanTable(handle));
+ Assertions.assertTrue(error.getMessage().contains("Paimon chain table
db.tbl"), error.getMessage());
+ Assertions.assertDoesNotThrow(() ->
provider().resolveScanTable(branch));
+ }
+
@Test
public void dataTableReachesTheBackendWithoutItsCatalogLoader(@TempDir
Path warehouse) {
VersionManagedCatalog catalog = new VersionManagedCatalog(warehouse,
false);
@@ -801,6 +849,16 @@ public class PaimonBackendBoundTableTest {
new org.apache.paimon.fs.Path("file://" + warehouse +
"/db.db/tbl"), schema, env);
}
+ /** A primary-key table object, as each branch of a chain table is; no
files are written. */
+ private static FileStoreTable primaryKeyTable(Path warehouse, String name,
CatalogEnvironment env) {
+ TableSchema schema = new TableSchema(0L,
+ Arrays.asList(new DataField(0, "id",
DataTypes.INT().notNull()),
+ new DataField(1, "v", DataTypes.STRING())),
+ 1, Collections.emptyList(), Collections.singletonList("id"),
Collections.emptyMap(), "");
+ return FileStoreTableFactory.create(LocalFileIO.create(),
+ new org.apache.paimon.fs.Path("file://" + warehouse +
"/db.db/" + name), schema, env);
+ }
+
/** A real, on-disk table so its {@code SnapshotManager} answers from real
snapshot files. */
private static FileStoreTable newRealTable(Path warehouse, String name)
throws Exception {
org.apache.paimon.fs.Path tablePath =
diff --git
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonReaderOptionsTest.java
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonReaderOptionsTest.java
index 8ad3796dc67..ac11a315f98 100644
---
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonReaderOptionsTest.java
+++
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonReaderOptionsTest.java
@@ -28,8 +28,10 @@ import org.apache.paimon.privilege.PrivilegedFileStoreTable;
import org.apache.paimon.schema.TableSchema;
import org.apache.paimon.table.AppendOnlyFileStoreTable;
import org.apache.paimon.table.CatalogEnvironment;
+import org.apache.paimon.table.ChainGroupReadTable;
import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.PrimaryKeyFileStoreTable;
import org.apache.paimon.table.Table;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.IntType;
@@ -265,6 +267,26 @@ public class PaimonReaderOptionsTest {
pair.other().options().get("scan.manifest.parallelism"));
}
+ @Test
+ void testRuntimeSafeTableKeepsTheChainGroupOfAChainTable() {
+ // Paimon builds a chain table as a fallback pair whose other branch
is a ChainGroupReadTable, and the outer
+ // pair accepts no other nested fallback. Capping a branch rebuilds
the pairs above it, so the chain group
+ // has to come back as one. MUTATION: rebuilding it as a plain
fallback pair -> red.
+ FileStoreTable snapshot = newPrimaryKeyTable("chain_snapshot",
+ ImmutableMap.of(CoreOptions.SCAN_MANIFEST_PARALLELISM.key(),
"8"));
+ FileStoreTable delta = newPrimaryKeyTable("chain_delta",
Collections.emptyMap());
+ Table chain = new
FallbackReadFileStoreTable(newPrimaryKeyTable("chain_main",
Collections.emptyMap()),
+ new ChainGroupReadTable(snapshot, delta), true);
+
+ Table safe = PaimonReaderOptions.runtimeSafeTable(chain, 4);
+
+ FileStoreTable group = ((FallbackReadFileStoreTable) safe).other();
+ Assertions.assertInstanceOf(ChainGroupReadTable.class, group);
+ Assertions.assertEquals("4", ((ChainGroupReadTable)
group).wrapped().options()
+ .get(CoreOptions.SCAN_MANIFEST_PARALLELISM.key()));
+ Assertions.assertSame(delta, ((ChainGroupReadTable) group).other());
+ }
+
@Test
void testBackendManifestCapUsesExecutionCeilingNotSmallestBranch() {
FileStoreTable main = newFileStoreTable("main_cap", ImmutableMap.of(
@@ -311,6 +333,19 @@ public class PaimonReaderOptionsTest {
LocalFileIO.create(), new Path("memory://" + name), schema,
CatalogEnvironment.empty());
}
+ private FileStoreTable newPrimaryKeyTable(String name, Map<String, String>
options) {
+ TableSchema schema = new TableSchema(
+ 0,
+ Collections.singletonList(new DataField(0, "id", new
IntType(false))),
+ 0,
+ Collections.emptyList(),
+ Collections.singletonList("id"),
+ options,
+ null);
+ return new PrimaryKeyFileStoreTable(
+ LocalFileIO.create(), new Path("memory://" + name), schema,
CatalogEnvironment.empty());
+ }
+
private FakePaimonTable fakeTable(String name, Map<String, String>
options) {
FakePaimonTable table = new FakePaimonTable(
name, org.apache.paimon.types.RowType.builder().field("id",
new IntType()).build(),
diff --git
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonStatementSchemaTest.java
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonStatementSchemaTest.java
index a488e37cd02..25bc3525853 100644
---
a/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonStatementSchemaTest.java
+++
b/fe/fe-connector/fe-connector-paimon/src/test/java/org/apache/doris/connector/paimon/PaimonStatementSchemaTest.java
@@ -30,6 +30,7 @@ import org.apache.doris.thrift.TFileScanRangeParams;
import org.apache.paimon.catalog.Catalog;
import org.apache.paimon.catalog.FileSystemCatalog;
import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.fs.FileIO;
@@ -40,6 +41,7 @@ import org.apache.paimon.privilege.PrivilegedFileStoreTable;
import org.apache.paimon.reader.RecordReader;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.table.ChainGroupReadTable;
import org.apache.paimon.table.DelegatedFileStoreTable;
import org.apache.paimon.table.FallbackReadFileStoreTable;
import org.apache.paimon.table.FileStoreTable;
@@ -722,6 +724,72 @@ public class PaimonStatementSchemaTest {
}
}
+ @Test
+ public void statementPinKeepsTheChainOfAChainTable(@TempDir Path
warehouse) throws Exception {
+ // A chain table reads a partition its snapshot branch lacks as the
latest earlier snapshot partition
+ // merged with the delta branch, through a ChainGroupReadTable nested
in its fallback pair. Restoring the
+ // pinned fallback snapshots and schemas rebuilds every pair, so the
group has to stay a group and still
+ // read the chain. MUTATION: rebuilding the chain group as a plain
fallback pair -> red.
+ try (Catalog catalog = new FileSystemCatalog(LocalFileIO.create(),
+ new org.apache.paimon.fs.Path(warehouse.toUri()))) {
+ catalog.createDatabase("db", false);
+ Identifier id = Identifier.create("db", "t");
+ catalog.createTable(id, Schema.newBuilder().column("id",
DataTypes.INT().notNull())
+ .column("value", DataTypes.STRING()).column("seq",
DataTypes.BIGINT())
+ .column("dt",
DataTypes.STRING().notNull()).primaryKey("dt", "id").partitionKeys("dt")
+ .option("bucket", "1").option("sequence.field",
"seq").option("file.format", "parquet")
+ .option("partition.timestamp-pattern",
"$dt").option("partition.timestamp-formatter", "yyyyMMdd")
+ .build(), false);
+ FileStoreTable main = (FileStoreTable) catalog.getTable(id);
+ main.createBranch("snapshot");
+ main.createBranch("delta");
+ // A pin maps each branch to its latest snapshot no later than the
one above it (main, then the snapshot
+ // branch), as the SDK does; commit in that order so every row is
visible at the pinned main snapshot.
+ append(main.switchToBranch("delta"), chainRow(2, "b2", 2L,
"20240102"));
+ append(main.switchToBranch("delta"), chainRow(3, "c", 2L,
"20240102"));
+ append(main.switchToBranch("snapshot"), chainRow(1, "a", 1L,
"20240101"));
+ append(main.switchToBranch("snapshot"), chainRow(2, "b", 1L,
"20240101"));
+ append(main, chainRow(9, "z", 1L, "20231231"));
+ List<SchemaChange> chainOptions =
Arrays.asList(SchemaChange.setOption("chain-table.enabled", "true"),
+ SchemaChange.setOption("scan.fallback-snapshot-branch",
"snapshot"),
+ SchemaChange.setOption("scan.fallback-delta-branch",
"delta"));
+ catalog.alterTable(id, chainOptions, false);
+ catalog.alterTable(new Identifier("db", "t", "snapshot"),
chainOptions, false);
+ catalog.alterTable(new Identifier("db", "t", "delta"),
chainOptions, false);
+ FileStoreTable table = (FileStoreTable) catalog.getTable(id);
+ Assertions.assertInstanceOf(ChainGroupReadTable.class,
((FallbackReadFileStoreTable) table).other());
+ long snapshotId = table.snapshotManager().latestSnapshotId();
+ Map<String, String> pinned = new
HashMap<>(PaimonScanParams.withBoundSchema(
+
PaimonScanParams.pinOptionsToSnapshot(Collections.emptyMap(), snapshotId),
table.schema().id()));
+ pinned.putAll(PaimonSchemaPin.capture(table, table.schema().id(),
snapshotId));
+
+ FileStoreTable restored =
PaimonScanParams.applyOptionsWithoutTimeTravel(table, pinned);
+
+ Assertions.assertInstanceOf(ChainGroupReadTable.class,
((FallbackReadFileStoreTable) restored).other());
+ List<String> expected = Arrays.asList("20231231 9 z", "20240101 1
a", "20240101 2 b",
+ "20240102 1 a", "20240102 2 b2", "20240102 3 c");
+ Assertions.assertEquals(expected, readChainRows(restored));
+ Assertions.assertEquals(expected,
readChainRows(table.copy(Collections.singletonMap(
+ "scan.snapshot-id", String.valueOf(snapshotId)))));
+ }
+ }
+
+ private static GenericRow chainRow(int id, String value, long seq, String
dt) {
+ return GenericRow.of(id, BinaryString.fromString(value), seq,
BinaryString.fromString(dt));
+ }
+
+ private static List<String> readChainRows(Table table) throws Exception {
+ List<String> rows = new ArrayList<>();
+ for (Split split : table.newReadBuilder().newScan().plan().splits()) {
+ try (RecordReader<InternalRow> reader =
table.newReadBuilder().newRead().createReader(split)) {
+ reader.forEachRemaining(row -> rows.add(row.getString(3) + " "
+ row.getInt(0) + " "
+ + row.getString(1)));
+ }
+ }
+ Collections.sort(rows);
+ return rows;
+ }
+
private void checkSchemaMutation(Path warehouse, boolean options, boolean
system) throws Exception {
try (Catalog catalog = new FileSystemCatalog(LocalFileIO.create(),
new org.apache.paimon.fs.Path(warehouse.toUri()))) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]