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 35f5d892cf [spark] Introduce reassign_row_id procedure (#9192)
35f5d892cf is described below
commit 35f5d892cf6854e4c3e2f69bf85d04d45536c942
Author: Xiangyi Zhu <[email protected]>
AuthorDate: Wed Aug 12 18:54:12 2026 +0800
[spark] Introduce reassign_row_id procedure (#9192)
---
docs/docs/spark/procedures.md | 12 ++
.../org/apache/paimon/spark/SparkProcedures.java | 2 +
.../spark/procedure/ReassignRowIdProcedure.java | 138 +++++++++++++++++++++
.../procedure/ReassignRowIdProcedureTest.scala | 134 ++++++++++++++++++++
4 files changed, 286 insertions(+)
diff --git a/docs/docs/spark/procedures.md b/docs/docs/spark/procedures.md
index 87a5d5826a..4e2a01c51c 100644
--- a/docs/docs/spark/procedures.md
+++ b/docs/docs/spark/procedures.md
@@ -574,6 +574,18 @@ This section introduce all available spark procedures
about paimon.
CALL sys.drop_global_index(table => 'default.T', index_column =>
'name', index_type => 'btree', dry_run => true)
</td>
</tr>
+ <tr>
+ <td>reassign_row_id</td>
+ <td>
+ To reassign row IDs for a data evolution table when partition row-id
ranges overlap. The table must have <code>row-tracking.enabled=true</code> and
<code>data-evolution.enabled=true</code>. Arguments:
+ <li>table: the target table identifier. Cannot be empty.</li>
+ <li>partitions: partition filter to limit the partitions to
reassign. The comma (",") represents "AND", the semicolon (";") represents
"OR". Left empty for all partitions.</li>
+ </td>
+ <td>
+ CALL sys.reassign_row_id(table => 'default.T')<br/><br/>
+ CALL sys.reassign_row_id(table => 'default.T', partitions =>
'dt=2026-05-19')
+ </td>
+ </tr>
<tr>
<td>copy</td>
<td>
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
index 3b12cf0936..60b5747e3d 100644
---
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
@@ -47,6 +47,7 @@ import
org.apache.paimon.spark.procedure.MigrateTableProcedure;
import org.apache.paimon.spark.procedure.Procedure;
import org.apache.paimon.spark.procedure.ProcedureBuilder;
import org.apache.paimon.spark.procedure.PurgeFilesProcedure;
+import org.apache.paimon.spark.procedure.ReassignRowIdProcedure;
import org.apache.paimon.spark.procedure.RemoveOrphanFilesProcedure;
import org.apache.paimon.spark.procedure.RemoveUnexistingFilesProcedure;
import org.apache.paimon.spark.procedure.RenameBranchProcedure;
@@ -132,6 +133,7 @@ public class SparkProcedures {
"trigger_tag_automatic_creation",
TriggerTagAutomaticCreationProcedure::builder);
procedureBuilders.put("rewrite_file_index",
RewriteFileIndexProcedure::builder);
procedureBuilders.put("copy", CopyFilesProcedure::builder);
+ procedureBuilders.put("reassign_row_id",
ReassignRowIdProcedure::builder);
return procedureBuilders.build();
}
}
diff --git
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ReassignRowIdProcedure.java
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ReassignRowIdProcedure.java
new file mode 100644
index 0000000000..8863ab56fe
--- /dev/null
+++
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ReassignRowIdProcedure.java
@@ -0,0 +1,138 @@
+/*
+ * 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.paimon.spark.procedure;
+
+import org.apache.paimon.append.dataevolution.DataEvolutionRowIdReassigner;
+import org.apache.paimon.partition.PartitionPredicate;
+import org.apache.paimon.spark.utils.SparkProcedureUtils;
+import org.apache.paimon.table.FileStoreTable;
+
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.types.Metadata;
+import org.apache.spark.sql.types.StructField;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.unsafe.types.UTF8String;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+import static org.apache.spark.sql.types.DataTypes.StringType;
+
+/**
+ * Reassign row id procedure. Reassigns row IDs for a data evolution table
when partition row-id
+ * ranges overlap. Usage:
+ *
+ * <pre><code>
+ * CALL sys.reassign_row_id(table => 'default.T')
+ * CALL sys.reassign_row_id(table => 'default.T', partitions =>
'dt=2026-05-19')
+ * </code></pre>
+ */
+public class ReassignRowIdProcedure extends BaseProcedure {
+
+ private static final ProcedureParameter[] PARAMETERS =
+ new ProcedureParameter[] {
+ ProcedureParameter.required("table", StringType),
+ ProcedureParameter.optional("partitions", StringType)
+ };
+
+ private static final StructType OUTPUT_TYPE =
+ new StructType(
+ new StructField[] {
+ new StructField("result", StringType, true,
Metadata.empty())
+ });
+
+ protected ReassignRowIdProcedure(TableCatalog tableCatalog) {
+ super(tableCatalog);
+ }
+
+ @Override
+ public ProcedureParameter[] parameters() {
+ return PARAMETERS;
+ }
+
+ @Override
+ public StructType outputType() {
+ return OUTPUT_TYPE;
+ }
+
+ @Override
+ public InternalRow[] call(InternalRow args) {
+ Identifier tableIdent = toIdentifier(args.getString(0),
PARAMETERS[0].name());
+ String partitions = args.isNullAt(1) ? null : args.getString(1);
+
+ return modifyPaimonTable(
+ tableIdent,
+ table -> {
+ checkArgument(
+ table instanceof FileStoreTable,
+ "Only FileStoreTable supports reassign_row_id
procedure. The table type is '%s'.",
+ table.getClass().getName());
+
+ FileStoreTable fileStoreTable = (FileStoreTable) table;
+ PartitionPredicate partitionPredicate =
+
SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+ partitions, fileStoreTable, spark());
+
+ DataEvolutionRowIdReassigner.Result result =
+ new DataEvolutionRowIdReassigner(fileStoreTable,
partitionPredicate)
+ .reassign();
+ String message = formatResult(tableIdent.toString(),
result);
+ return new InternalRow[]
{newInternalRow(UTF8String.fromString(message))};
+ });
+ }
+
+ private static String formatResult(
+ String tableName, DataEvolutionRowIdReassigner.Result result) {
+ if (!result.reassigned) {
+ String reason =
+ result.skipReason == null
+ ? "row IDs are already partition-contiguous"
+ : result.skipReason;
+ return String.format(
+ "Skipped. Row IDs for table '%s' were not reassigned
because %s: "
+ + "snapshot %d unchanged, nextRowId=%d.",
+ tableName, reason, result.previousSnapshotId,
result.nextRowId);
+ }
+ return String.format(
+ "Success. Reassigned row IDs for table '%s': snapshot %d ->
%d, "
+ + "nextRowId %d -> %d, files=%d, rows=%d,
indexFiles=%d.",
+ tableName,
+ result.previousSnapshotId,
+ result.newSnapshotId,
+ result.firstAssignedRowId,
+ result.nextRowId,
+ result.fileCount,
+ result.rowCount,
+ result.indexFileCount);
+ }
+
+ public static ProcedureBuilder builder() {
+ return new Builder<ReassignRowIdProcedure>() {
+ @Override
+ public ReassignRowIdProcedure doBuild() {
+ return new ReassignRowIdProcedure(tableCatalog());
+ }
+ };
+ }
+
+ @Override
+ public String description() {
+ return "ReassignRowIdProcedure";
+ }
+}
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/ReassignRowIdProcedureTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/ReassignRowIdProcedureTest.scala
new file mode 100644
index 0000000000..f6daa63fbc
--- /dev/null
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/ReassignRowIdProcedureTest.scala
@@ -0,0 +1,134 @@
+/*
+ * 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.paimon.spark.procedure
+
+import org.apache.paimon.spark.PaimonSparkTestBase
+
+import org.apache.spark.sql.Row
+import org.assertj.core.api.Assertions.assertThatThrownBy
+
+/** IT Case for [[ReassignRowIdProcedure]]. */
+class ReassignRowIdProcedureTest extends PaimonSparkTestBase {
+
+ test("Paimon Procedure: reassign row ids for interleaved partitions") {
+ withTable("t") {
+ sql(s"""
+ |CREATE TABLE t (id INT, pt STRING)
+ |TBLPROPERTIES (
+ | 'row-tracking.enabled' = 'true',
+ | 'data-evolution.enabled' = 'true')
+ |PARTITIONED BY (pt)
+ |""".stripMargin)
+ // each INSERT is its own commit, so row ids are assigned in commit
order and
+ // interleave across the 'a' and 'b' partitions
+ sql("INSERT INTO t VALUES (0, 'a')")
+ sql("INSERT INTO t VALUES (1, 'b')")
+ sql("INSERT INTO t VALUES (2, 'a')")
+ sql("INSERT INTO t VALUES (3, 'b')")
+ sql("INSERT INTO t VALUES (4, 'a')")
+
+ checkAnswer(
+ sql("SELECT id, pt, _ROW_ID FROM t ORDER BY id"),
+ Seq(Row(0, "a", 0), Row(1, "b", 1), Row(2, "a", 2), Row(3, "b", 3),
Row(4, "a", 4)))
+
+ val result = sql("CALL sys.reassign_row_id(table => 't')").collect()
+ assert(result.length == 1)
+ assert(
+ result(0).getString(0).startsWith("Success."),
+ s"Unexpected result: ${result(0).getString(0)}")
+
+ // row ids are now contiguous within each partition, data is unaffected
+ checkAnswer(
+ sql("SELECT id, pt, _ROW_ID FROM t ORDER BY id"),
+ Seq(Row(0, "a", 5), Row(1, "b", 8), Row(2, "a", 6), Row(3, "b", 9),
Row(4, "a", 7)))
+
+ // calling again on already-contiguous row ids is a no-op
+ val second = sql("CALL sys.reassign_row_id(table => 't')").collect()
+ assert(
+ second(0).getString(0).startsWith("Skipped."),
+ s"Unexpected result: ${second(0).getString(0)}")
+ }
+ }
+
+ test("Paimon Procedure: reassign row id with partitions filter") {
+ withTable("t") {
+ sql(s"""
+ |CREATE TABLE t (id INT, pt STRING)
+ |TBLPROPERTIES (
+ | 'row-tracking.enabled' = 'true',
+ | 'data-evolution.enabled' = 'true')
+ |PARTITIONED BY (pt)
+ |""".stripMargin)
+ sql("INSERT INTO t VALUES (0, 'a')")
+ sql("INSERT INTO t VALUES (1, 'b')")
+ sql("INSERT INTO t VALUES (2, 'a')")
+
+ // no partition matches this filter, so nothing needs to be reassigned
+ val skipped =
+ sql("CALL sys.reassign_row_id(table => 't', partitions =>
'pt=c')").collect()
+ assert(
+ skipped(0).getString(0).startsWith("Skipped."),
+ s"Unexpected result: ${skipped(0).getString(0)}")
+
+ val result =
+ sql("CALL sys.reassign_row_id(table => 't', partitions =>
'pt=a')").collect()
+ assert(
+ result(0).getString(0).startsWith("Success."),
+ s"Unexpected result: ${result(0).getString(0)}")
+
+ checkAnswer(
+ sql("SELECT id, pt FROM t ORDER BY id"),
+ Seq(Row(0, "a"), Row(1, "b"), Row(2, "a")))
+ }
+ }
+
+ test("Paimon Procedure: reassign row id requires row tracking enabled") {
+ withTable("t") {
+ sql("CREATE TABLE t (id INT, pt STRING) PARTITIONED BY (pt)")
+ sql("INSERT INTO t VALUES (0, 'a')")
+
+ assertThatThrownBy(() => sql("CALL sys.reassign_row_id(table => 't')"))
+ .hasMessageContaining("row-tracking.enabled=true")
+ }
+ }
+
+ test("Paimon Procedure: reassign row id requires data evolution enabled") {
+ withTable("t") {
+ sql(
+ "CREATE TABLE t (id INT, pt STRING) TBLPROPERTIES
('row-tracking.enabled' = 'true') PARTITIONED BY (pt)")
+ sql("INSERT INTO t VALUES (0, 'a')")
+
+ assertThatThrownBy(() => sql("CALL sys.reassign_row_id(table => 't')"))
+ .hasMessageContaining("data-evolution.enabled=true")
+ }
+ }
+
+ test("Paimon Procedure: reassign row id skips non-partitioned table") {
+ withTable("t") {
+ sql(
+ "CREATE TABLE t (id INT) TBLPROPERTIES ('row-tracking.enabled' =
'true', 'data-evolution.enabled' = 'true')")
+ sql("INSERT INTO t VALUES (1)")
+
+ val result = sql("CALL sys.reassign_row_id(table => 't')").collect()
+ assert(
+ result(0).getString(0).contains("table is not partitioned"),
+ s"Unexpected result: ${result(0).getString(0)}")
+ }
+ }
+}