This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new aea054381c [core] Return empty schemas/snapshots table result for
out-of-range id filters (#10206)
aea054381c is described below
commit aea054381c3646159b8c2e9152c21c4f400dfed5
Author: YangJie <[email protected]>
AuthorDate: Mon Oct 5 10:21:24 2026 -0400
[core] Return empty schemas/snapshots table result for out-of-range id
filters (#10206)
---
.../apache/paimon/table/system/SchemasTable.java | 12 ++--
.../org/apache/paimon/utils/SnapshotManager.java | 12 ++--
.../paimon/table/system/SchemasTableTest.java | 69 ++++++++++++++++++----
.../paimon/table/system/SnapshotsTableTest.java | 48 +++++++++++++++
.../apache/paimon/flink/CatalogTableITCase.java | 15 +++--
.../paimon/hive/FlinkGenericCatalogITCase.java | 15 ++---
.../paimon/spark/sql/PaimonSystemTableTest.scala | 12 ++++
7 files changed, 138 insertions(+), 45 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/system/SchemasTable.java
b/paimon-core/src/main/java/org/apache/paimon/table/system/SchemasTable.java
index 9c3fd9ca61..7e09f55b80 100644
--- a/paimon-core/src/main/java/org/apache/paimon/table/system/SchemasTable.java
+++ b/paimon-core/src/main/java/org/apache/paimon/table/system/SchemasTable.java
@@ -345,10 +345,8 @@ public class SchemasTable implements ReadonlyTable {
if (optionalMaxSchemaId != null) {
if (optionalMaxSchemaId < lowerBoundSchemaId) {
- throw new RuntimeException(
- String.format(
- "schema id: %s should not lower than min
schema id: %s",
- optionalMaxSchemaId, lowerBoundSchemaId));
+ // schema ids lie in [0, latest], so the range matches none of
them
+ return Collections.emptyList();
}
upperBoundSchematId =
optionalMaxSchemaId > upperBoundSchematId
@@ -358,10 +356,8 @@ public class SchemasTable implements ReadonlyTable {
if (optionalMinSchemaId != null) {
if (optionalMinSchemaId > upperBoundSchematId) {
- throw new RuntimeException(
- String.format(
- "schema id: %s should not greater than max
schema id: %s",
- optionalMinSchemaId, upperBoundSchematId));
+ // schema ids lie in [0, latest], so the range matches none of
them
+ return Collections.emptyList();
}
lowerBoundSchemaId =
optionalMinSchemaId > lowerBoundSchemaId
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
b/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
index 18740a9a3f..5f90d2db91 100644
--- a/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
+++ b/paimon-core/src/main/java/org/apache/paimon/utils/SnapshotManager.java
@@ -628,10 +628,8 @@ public class SnapshotManager implements Serializable {
if (optionalMaxSnapshotId.isPresent()) {
upperId = optionalMaxSnapshotId.get();
if (upperId < lowerBoundSnapshotId) {
- throw new RuntimeException(
- String.format(
- "snapshot upper id:%s should not greater than
earliestSnapshotId:%s",
- upperId, lowerBoundSnapshotId));
+ // no snapshot id in [earliest, latest] falls in the range
+ return Collections.emptyIterator();
}
upperBoundSnapshotId = upperId < upperBoundSnapshotId ? upperId :
upperBoundSnapshotId;
}
@@ -639,10 +637,8 @@ public class SnapshotManager implements Serializable {
if (optionalMinSnapshotId.isPresent()) {
lowerId = optionalMinSnapshotId.get();
if (lowerId > upperBoundSnapshotId) {
- throw new RuntimeException(
- String.format(
- "snapshot upper id:%s should not greater than
latestSnapshotId:%s",
- lowerId, upperBoundSnapshotId));
+ // no snapshot id in [earliest, latest] falls in the range
+ return Collections.emptyIterator();
}
lowerBoundSnapshotId = lowerId > lowerBoundSnapshotId ? lowerId :
lowerBoundSnapshotId;
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/system/SchemasTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/system/SchemasTableTest.java
index 5df2d5d8e6..dada6f00b2 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/system/SchemasTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/system/SchemasTableTest.java
@@ -32,6 +32,7 @@ import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.schema.FileSystemSchemaManager;
import org.apache.paimon.schema.Schema;
+import org.apache.paimon.schema.SchemaChange;
import org.apache.paimon.schema.SchemaManager;
import org.apache.paimon.schema.TableSchema;
import org.apache.paimon.table.TableTestBase;
@@ -119,24 +120,68 @@ public class SchemasTableTest extends TableTestBase {
assertThat(result).isEmpty();
}
+ @Test
+ public void testReadSchemasWithOutOfRangeFilterReturnsEmpty() throws
Exception {
+ // commit a second schema so the latest schema id is 1
+ catalog.alterTable(identifier("T"), SchemaChange.setOption("k", "v"),
false);
+ assertThat(read(schemasTable)).hasSize(2);
+
+ PredicateBuilder builder = new
PredicateBuilder(schemasTable.rowType());
+ int schemaIdIdx =
schemasTable.rowType().getFieldNames().indexOf("schema_id");
+
+ List<Predicate> outOfRange = new ArrayList<>();
+ // lower bound above the latest schema id
+ outOfRange.add(builder.greaterThan(schemaIdIdx, 99L));
+ // upper bound below the first schema id
+ outOfRange.add(builder.lessThan(schemaIdIdx, -1L));
+ // lower bound above the upper bound, both within [0, latest]
+ outOfRange.add(
+ PredicateBuilder.and(
+ builder.greaterThan(schemaIdIdx, 0L),
builder.lessThan(schemaIdIdx, 1L)));
+
+ for (Predicate predicate : outOfRange) {
+ assertThat(readWithFilter(predicate)).isEmpty();
+ }
+
+ // the bounds of [0, latest] still return their schema
+ assertThat(readWithFilter(builder.lessOrEqual(schemaIdIdx, 0L)))
+ .containsExactly(toExpectedRow(schemaManager.schema(0L)));
+ assertThat(readWithFilter(builder.greaterOrEqual(schemaIdIdx, 1L)))
+ .containsExactly(toExpectedRow(schemaManager.schema(1L)));
+ }
+
+ private List<InternalRow> readWithFilter(Predicate predicate) throws
Exception {
+ ReadBuilder readBuilder =
schemasTable.newReadBuilder().withFilter(predicate);
+ List<InternalRow> result = new ArrayList<>();
+ InternalRowSerializer serializer = new
InternalRowSerializer(schemasTable.rowType());
+ readBuilder
+ .newRead()
+ .createReader(readBuilder.newScan().plan())
+ .forEachRemaining(row -> result.add(serializer.copy(row)));
+ return result;
+ }
+
private List<InternalRow> getExpectedResult() {
List<TableSchema> tableSchemas = schemaManager.listAll();
List<InternalRow> expectedRow = new ArrayList<>();
for (TableSchema schema : tableSchemas) {
- expectedRow.add(
- GenericRow.of(
- schema.id(),
-
BinaryString.fromString(toFlatJson(schema.fields())),
-
BinaryString.fromString(toFlatJson(schema.partitionKeys())),
-
BinaryString.fromString(toFlatJson(schema.primaryKeys())),
-
BinaryString.fromString(toFlatJson(schema.options())),
- BinaryString.fromString(schema.comment()),
- Timestamp.fromLocalDateTime(
- LocalDateTime.ofInstant(
-
Instant.ofEpochMilli(schema.timeMillis()),
- ZoneId.systemDefault()))));
+ expectedRow.add(toExpectedRow(schema));
}
return expectedRow;
}
+
+ private static InternalRow toExpectedRow(TableSchema schema) {
+ return GenericRow.of(
+ schema.id(),
+ BinaryString.fromString(toFlatJson(schema.fields())),
+ BinaryString.fromString(toFlatJson(schema.partitionKeys())),
+ BinaryString.fromString(toFlatJson(schema.primaryKeys())),
+ BinaryString.fromString(toFlatJson(schema.options())),
+ BinaryString.fromString(schema.comment()),
+ Timestamp.fromLocalDateTime(
+ LocalDateTime.ofInstant(
+ Instant.ofEpochMilli(schema.timeMillis()),
+ ZoneId.systemDefault())));
+ }
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/system/SnapshotsTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/system/SnapshotsTableTest.java
index 648344b2b8..e04da130b6 100644
---
a/paimon-core/src/test/java/org/apache/paimon/table/system/SnapshotsTableTest.java
+++
b/paimon-core/src/test/java/org/apache/paimon/table/system/SnapshotsTableTest.java
@@ -29,6 +29,7 @@ import
org.apache.paimon.data.serializer.InternalRowSerializer;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.options.ExpireConfig;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.schema.FileSystemSchemaManager;
@@ -45,6 +46,7 @@ import org.apache.paimon.utils.SnapshotManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -136,6 +138,52 @@ public class SnapshotsTableTest extends TableTestBase {
assertThat(result).isEmpty();
}
+ @Test
+ public void testReadSnapshotsWithOutOfRangeFilterReturnsEmpty() throws
Exception {
+ PredicateBuilder builder = new
PredicateBuilder(snapshotsTable.rowType());
+ int snapshotIdIdx =
snapshotsTable.rowType().getFieldNames().indexOf("snapshot_id");
+
+ // snapshots 1 and 2 exist
+ // lower bound above the latest snapshot id
+ assertThat(readWithFilter(builder.greaterThan(snapshotIdIdx,
99L))).isEmpty();
+ // upper bound below the earliest snapshot id
+ assertThat(readWithFilter(builder.lessThan(snapshotIdIdx,
1L))).isEmpty();
+ // lower bound above the upper bound, both within [earliest, latest]
+ assertThat(
+ readWithFilter(
+ PredicateBuilder.and(
+ builder.greaterThan(snapshotIdIdx, 1L),
+ builder.lessThan(snapshotIdIdx, 2L))))
+ .isEmpty();
+
+ // expire snapshot 1, then query a range that ends below the new
earliest snapshot
+ FileStoreTable table = (FileStoreTable)
catalog.getTable(identifier(tableName));
+ table.newExpireSnapshots()
+ .config(
+ ExpireConfig.builder()
+ .snapshotRetainMax(1)
+ .snapshotRetainMin(1)
+ .snapshotTimeRetain(Duration.ZERO)
+ .snapshotMaxDeletes(Integer.MAX_VALUE)
+ .build())
+ .expire();
+ assertThat(snapshotManager.earliestSnapshotId()).isEqualTo(2L);
+ assertThat(readWithFilter(builder.lessOrEqual(snapshotIdIdx,
1L))).isEmpty();
+ assertThat(readWithFilter(builder.lessOrEqual(snapshotIdIdx, 2L)))
+ .containsExactlyElementsOf(getExpectedResult(new long[] {2}));
+ }
+
+ private List<InternalRow> readWithFilter(Predicate predicate) throws
Exception {
+ ReadBuilder readBuilder =
snapshotsTable.newReadBuilder().withFilter(predicate);
+ List<InternalRow> result = new ArrayList<>();
+ InternalRowSerializer serializer = new
InternalRowSerializer(snapshotsTable.rowType());
+ readBuilder
+ .newRead()
+ .createReader(readBuilder.newScan().plan())
+ .forEachRemaining(row -> result.add(serializer.copy(row)));
+ return result;
+ }
+
private List<InternalRow> getExpectedResult(long[] snapshotIds) {
List<InternalRow> expectedRow = new ArrayList<>();
for (long snapshotId : snapshotIds) {
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/CatalogTableITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/CatalogTableITCase.java
index 9024edeca5..92cf7e5385 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/CatalogTableITCase.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/CatalogTableITCase.java
@@ -385,14 +385,13 @@ public class CatalogTableITCase extends CatalogITCaseBase
{
+ "primary_keys, options, `comment`
FROM T$schemas where schema_id = 5"))
.isEmpty();
- // check with not exist schema id
- assertThatThrownBy(
- () ->
- sql(
- "SELECT schema_id, fields,
partition_keys, "
- + "primary_keys, options,
`comment` FROM T$schemas where schema_id>=6"))
- .hasCauseInstanceOf(RuntimeException.class)
- .hasRootCauseMessage("schema id: 6 should not greater than max
schema id: 4");
+ // check with out-of-range schema id: no schema has id >= 6 (latest is
4), so the
+ // query returns zero rows instead of failing
+ assertThat(
+ sql(
+ "SELECT schema_id, fields, partition_keys, "
+ + "primary_keys, options, `comment`
FROM T$schemas where schema_id>=6"))
+ .isEmpty();
}
@Test
diff --git
a/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/FlinkGenericCatalogITCase.java
b/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/FlinkGenericCatalogITCase.java
index b48404fb1c..3bd7792243 100644
---
a/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/FlinkGenericCatalogITCase.java
+++
b/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/FlinkGenericCatalogITCase.java
@@ -46,7 +46,6 @@ import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
/** IT cases for using Flink {@code FlinkGenericCatalog}. */
@RunWith(PaimonEmbeddedHiveRunner.class)
@@ -179,14 +178,12 @@ public class FlinkGenericCatalogITCase extends
AbstractTestBaseJUnit4 {
Row.of(3L, 0L, "APPEND"),
Row.of(4L, 0L, "APPEND"));
- // check with wrong range
- assertThatThrownBy(
- () ->
- sql(
- "SELECT snapshot_id, schema_id,
commit_kind FROM paimon_t$snapshots where snapshot_id>9"))
- .hasCauseInstanceOf(RuntimeException.class)
- .hasRootCauseMessage(
- "snapshot upper id:10 should not greater than
latestSnapshotId:4");
+ // check with out-of-range snapshot id: the filter matches no
snapshot, so the query
+ // returns zero rows instead of failing
+ assertThat(
+ sql(
+ "SELECT snapshot_id, schema_id, commit_kind
FROM paimon_t$snapshots where snapshot_id>9"))
+ .isEmpty();
}
@Test
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonSystemTableTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonSystemTableTest.scala
index 12a18d3761..48f8c3b269 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonSystemTableTest.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonSystemTableTest.scala
@@ -125,4 +125,16 @@ class PaimonSystemTableTest extends PaimonSparkTestBase {
Seq(Row("+I", Array(3)), Row("+I", Array(2)))
)
}
+
+ test("system table: snapshots and schemas tables with out-of-range id
filter") {
+ sql("CREATE TABLE T (a INT, b STRING)")
+ sql("INSERT INTO T VALUES (1, 'a')")
+ sql("INSERT INTO T VALUES (2, 'b')")
+
+ checkAnswer(sql("SELECT snapshot_id FROM `T$snapshots` WHERE snapshot_id >
2"), Nil)
+ checkAnswer(sql("SELECT schema_id FROM `T$schemas` WHERE schema_id > 0"),
Nil)
+
+ sql("CALL paimon.sys.expire_snapshots(table => 'test.T', retain_max => 1,
retain_min => 1)")
+ checkAnswer(sql("SELECT snapshot_id FROM `T$snapshots` WHERE snapshot_id <
2"), Nil)
+ }
}