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 52b8e8adac [spark] Support create_tag_from_watermark procedure (#10420)
52b8e8adac is described below

commit 52b8e8adac8972a4be1bc5ecb7f08b929f57d4d8
Author: Wenchao Wu <[email protected]>
AuthorDate: Tue Oct 13 13:31:27 2026 +0800

    [spark] Support create_tag_from_watermark procedure (#10420)
---
 docs/docs/spark/procedures.md                      |   1 +
 docs/docs/spark/procedures/versions.md             |  20 +++
 .../org/apache/paimon/utils/SnapshotManager.java   |   7 +-
 .../apache/paimon/utils/SnapshotManagerTest.java   |  54 ++++++
 .../org/apache/paimon/spark/SparkProcedures.java   |   3 +
 .../procedure/CreateTagFromWatermarkProcedure.java | 139 ++++++++++++++++
 .../CreateTagFromWatermarkProcedureTest.scala      | 184 +++++++++++++++++++++
 7 files changed, 406 insertions(+), 2 deletions(-)

diff --git a/docs/docs/spark/procedures.md b/docs/docs/spark/procedures.md
index 70f329b3a4..8e209a484f 100644
--- a/docs/docs/spark/procedures.md
+++ b/docs/docs/spark/procedures.md
@@ -79,6 +79,7 @@ Choose a group, then use the page contents to jump to a 
procedure:
 
 [`create_tag`](./procedures/versions#create_tag),
 [`create_tag_from_timestamp`](./procedures/versions#create_tag_from_timestamp),
+[`create_tag_from_watermark`](./procedures/versions#create_tag_from_watermark),
 [`replace_tag`](./procedures/versions#replace_tag),
 [`rename_tag`](./procedures/versions#rename_tag),
 [`delete_tag`](./procedures/versions#delete_tag),
diff --git a/docs/docs/spark/procedures/versions.md 
b/docs/docs/spark/procedures/versions.md
index f4d4a76f08..7b007c527b 100644
--- a/docs/docs/spark/procedures/versions.md
+++ b/docs/docs/spark/procedures/versions.md
@@ -69,6 +69,26 @@ CALL sys.create_tag_from_timestamp(
 );
 ```
 
+## create_tag_from_watermark
+
+Create a tag from the first retained snapshot (including tagged snapshots) 
whose watermark is at or after the given value.
+
+**Arguments**
+
+- `table` (`STRING`, required): the target table identifier.
+- `tag` (`STRING`, required): name of the new tag.
+- `watermark` (`BIGINT`, required): find the first snapshot whose watermark is 
later than or equal to this value.
+- `time_retained` (`STRING`, optional): The maximum time retained for newly 
created tags.
+
+```sql
+CALL sys.create_tag_from_watermark(
+  `table` => 'default.T',
+  `tag` => 'my_tag',
+  `watermark` => 1724404318750,
+  time_retained => '1 d'
+);
+```
+
 ## replace_tag
 
 Replace an existing tag with new tag info.
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 5f90d2db91..df627955e5 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
@@ -558,8 +558,11 @@ public class SnapshotManager implements Serializable {
                 } else if (commitWatermark < watermark) {
                     earliest = mid + 1; // Search in the right half
                 } else {
-                    finalSnapshot = snapshot; // Found the exact match
-                    break;
+                    // Lower bound: an exact match is a candidate, but an 
earlier snapshot may
+                    // carry the same watermark. Watermarks repeat because the 
commit watermark
+                    // takes the maximum, so stopping here would return the 
middle of such a run.
+                    finalSnapshot = snapshot;
+                    latest = pos - 1; // Keep searching the left half
                 }
             }
         }
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java 
b/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
index fb09951952..9bd9444bd5 100644
--- a/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/utils/SnapshotManagerTest.java
@@ -855,6 +855,60 @@ public class SnapshotManagerTest {
         assertThat(snapshotManager.laterOrEqualWatermark(250)).isNull();
     }
 
+    @Test
+    public void testLaterOrEqualWatermarkWithRepeatedWatermarks() throws 
IOException {
+        FileIO localFileIO = LocalFileIO.create();
+        SnapshotManager snapshotManager =
+                newSnapshotManager(localFileIO, new Path(tempDir.toString()));
+        // a plateau of identical watermarks: snapshots 1, 2, 3 all carry 
1000. Repeated
+        // watermarks are normal because the commit watermark takes the 
maximum.
+        Map<Long, Long> watermarks = watermarkMap(0, 500, 1, 1000, 2, 1000, 3, 
1000, 4, 2000);
+        for (long i = 0; i < 5; i++) {
+            Snapshot snapshot = createSnapshotWithMillis(i, 1000 + i * 1000, 
watermarks.get(i));
+            localFileIO.tryToWriteAtomic(snapshotManager.snapshotPath(i), 
snapshot.toJson());
+        }
+
+        // lower bound: the first snapshot of the plateau, not the 
binary-search midpoint
+        
assertThat(snapshotManager.laterOrEqualWatermark(1000).id()).isEqualTo(1);
+        // a value inside the gap still resolves to the start of the next 
plateau
+        
assertThat(snapshotManager.laterOrEqualWatermark(501).id()).isEqualTo(1);
+        // non-repeated watermarks are unaffected
+        
assertThat(snapshotManager.laterOrEqualWatermark(2000).id()).isEqualTo(4);
+        
assertThat(snapshotManager.laterOrEqualWatermark(500).id()).isEqualTo(0);
+    }
+
+    @Test
+    public void testLaterOrEqualWatermarkWithPlateauAtTheEarliestSnapshot() 
throws IOException {
+        FileIO localFileIO = LocalFileIO.create();
+        SnapshotManager snapshotManager =
+                newSnapshotManager(localFileIO, new Path(tempDir.toString()));
+        // the plateau starts at the earliest snapshot, so the left walk must 
stop there
+        Map<Long, Long> watermarks = watermarkMap(0, 1000, 1, 1000, 2, 1000, 
3, 2000);
+        for (long i = 0; i < 4; i++) {
+            Snapshot snapshot = createSnapshotWithMillis(i, 1000 + i * 1000, 
watermarks.get(i));
+            localFileIO.tryToWriteAtomic(snapshotManager.snapshotPath(i), 
snapshot.toJson());
+        }
+
+        
assertThat(snapshotManager.laterOrEqualWatermark(1000).id()).isEqualTo(0);
+    }
+
+    @Test
+    public void 
testLaterOrEqualWatermarkWithRepeatedWatermarksAndNullWatermarks()
+            throws IOException {
+        FileIO localFileIO = LocalFileIO.create();
+        SnapshotManager snapshotManager =
+                newSnapshotManager(localFileIO, new Path(tempDir.toString()));
+        // snapshots 2 and 4 carry no watermark and sit inside a plateau of 
1000s
+        Map<Long, Long> watermarks = watermarkMap(0, 500, 1, 1000, 3, 1000, 5, 
1000, 6, 2000);
+        for (long i = 0; i < 7; i++) {
+            Snapshot snapshot = createSnapshotWithMillis(i, 1000 + i * 1000, 
watermarks.get(i));
+            localFileIO.tryToWriteAtomic(snapshotManager.snapshotPath(i), 
snapshot.toJson());
+        }
+
+        // null-watermark snapshots must not shadow the real start of the 
plateau
+        
assertThat(snapshotManager.laterOrEqualWatermark(1000).id()).isEqualTo(1);
+    }
+
     @Test
     public void testEarlierOrEqualWatermarkBelowMinimum() throws IOException {
         FileIO localFileIO = LocalFileIO.create();
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 61d318a5d9..3ecd59c8bf 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
@@ -31,6 +31,7 @@ import 
org.apache.paimon.spark.procedure.CreateFunctionProcedure;
 import org.apache.paimon.spark.procedure.CreateGlobalIndexProcedure;
 import org.apache.paimon.spark.procedure.CreatePolicyProcedure;
 import org.apache.paimon.spark.procedure.CreateTagFromTimestampProcedure;
+import org.apache.paimon.spark.procedure.CreateTagFromWatermarkProcedure;
 import org.apache.paimon.spark.procedure.CreateTagProcedure;
 import org.apache.paimon.spark.procedure.DeleteBranchProcedure;
 import org.apache.paimon.spark.procedure.DeleteTagProcedure;
@@ -110,6 +111,8 @@ public class SparkProcedures {
         procedureBuilders.put("rename_tag", RenameTagProcedure::builder);
         procedureBuilders.put(
                 "create_tag_from_timestamp", 
CreateTagFromTimestampProcedure::builder);
+        procedureBuilders.put(
+                "create_tag_from_watermark", 
CreateTagFromWatermarkProcedure::builder);
         procedureBuilders.put("delete_tag", DeleteTagProcedure::builder);
         procedureBuilders.put("expire_tags", ExpireTagsProcedure::builder);
         procedureBuilders.put("create_branch", CreateBranchProcedure::builder);
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateTagFromWatermarkProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateTagFromWatermarkProcedure.java
new file mode 100644
index 0000000000..214787f20e
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateTagFromWatermarkProcedure.java
@@ -0,0 +1,139 @@
+/*
+ * 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.Snapshot;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.utils.SnapshotManager;
+import org.apache.paimon.utils.SnapshotNotExistException;
+import org.apache.paimon.utils.TimeUtils;
+
+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.DataTypes;
+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 java.time.Duration;
+import java.util.Set;
+
+import static org.apache.spark.sql.types.DataTypes.LongType;
+import static org.apache.spark.sql.types.DataTypes.StringType;
+
+/** The procedure supports creating tags from snapshot watermarks. */
+public class CreateTagFromWatermarkProcedure extends BaseProcedure {
+
+    private static final ProcedureParameter[] PARAMETERS =
+            new ProcedureParameter[] {
+                ProcedureParameter.required("table", StringType),
+                ProcedureParameter.required("tag", StringType),
+                ProcedureParameter.required("watermark", LongType),
+                ProcedureParameter.optional("time_retained", StringType)
+            };
+
+    private static final StructType OUTPUT_TYPE =
+            new StructType(
+                    new StructField[] {
+                        new StructField("tagName", DataTypes.StringType, true, 
Metadata.empty()),
+                        new StructField("snapshot", DataTypes.LongType, true, 
Metadata.empty()),
+                        new StructField("commit_time", DataTypes.LongType, 
true, Metadata.empty()),
+                        new StructField("watermark", DataTypes.StringType, 
true, Metadata.empty())
+                    });
+
+    private CreateTagFromWatermarkProcedure(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 tag = args.getString(1);
+        Long watermark = args.getLong(2);
+        Duration timeRetained =
+                args.isNullAt(3) ? null : 
TimeUtils.parseDuration(args.getString(3));
+
+        return modifyPaimonTable(
+                tableIdent,
+                table -> {
+                    FileStoreTable fileStoreTable = (FileStoreTable) table;
+                    SnapshotManager snapshotManager = 
fileStoreTable.snapshotManager();
+                    Snapshot snapshot = 
snapshotManager.laterOrEqualWatermark(watermark);
+
+                    Set<Snapshot> sortedTagsSnapshots = 
fileStoreTable.tagManager().tags().keySet();
+                    for (Snapshot tagSnapshot : sortedTagsSnapshots) {
+                        if (tagSnapshot.watermark() != null
+                                && watermark <= tagSnapshot.watermark()) {
+                            // Equal watermarks keep the earlier snapshot. 
Comparing with <=
+                            // would replace an earlier live snapshot with a 
later tag.
+                            if (snapshot == null
+                                    || snapshot.watermark() == null
+                                    || tagSnapshot.watermark() < 
snapshot.watermark()
+                                    || 
(tagSnapshot.watermark().equals(snapshot.watermark())
+                                            && tagSnapshot.id() < 
snapshot.id())) {
+                                snapshot = tagSnapshot;
+                            }
+                            break;
+                        }
+                    }
+
+                    SnapshotNotExistException.checkNotNull(
+                            snapshot,
+                            String.format(
+                                    "Could not find any snapshot whose 
watermark later than %s.",
+                                    watermark));
+
+                    fileStoreTable.createTag(tag, snapshot.id(), timeRetained);
+
+                    InternalRow outputRow =
+                            newInternalRow(
+                                    UTF8String.fromString(tag),
+                                    snapshot.id(),
+                                    snapshot.timeMillis(),
+                                    
UTF8String.fromString(String.valueOf(snapshot.watermark())));
+                    return new InternalRow[] {outputRow};
+                });
+    }
+
+    public static ProcedureBuilder builder() {
+        return new BaseProcedure.Builder<CreateTagFromWatermarkProcedure>() {
+            @Override
+            public CreateTagFromWatermarkProcedure doBuild() {
+                return new CreateTagFromWatermarkProcedure(tableCatalog());
+            }
+        };
+    }
+
+    @Override
+    public String description() {
+        return "CreateTagFromWatermarkProcedure";
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CreateTagFromWatermarkProcedureTest.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CreateTagFromWatermarkProcedureTest.scala
new file mode 100644
index 0000000000..90f01f93dd
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CreateTagFromWatermarkProcedureTest.scala
@@ -0,0 +1,184 @@
+/*
+ * 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.data.{BinaryString, GenericRow}
+import org.apache.paimon.manifest.ManifestCommittable
+import org.apache.paimon.spark.PaimonSparkTestBase
+import org.apache.paimon.table.sink.TableCommitImpl
+import org.apache.paimon.utils.SnapshotNotExistException
+
+import org.apache.spark.sql.Row
+
+class CreateTagFromWatermarkProcedureTest extends PaimonSparkTestBase {
+
+  test("Paimon Procedure: create tags from snapshot watermarks") {
+    spark.sql("CREATE TABLE T (k INT, v STRING)")
+
+    writeWithWatermark("T", null, GenericRow.of(Integer.valueOf(1), 
BinaryString.fromString("a")))
+
+    intercept[SnapshotNotExistException] {
+      spark.sql(
+        "CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'tag1', 
watermark => 1000)")
+    }
+
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(2), 
BinaryString.fromString("b")))
+    writeWithWatermark("T", 2000L, GenericRow.of(Integer.valueOf(3), 
BinaryString.fromString("c")))
+
+    val table = loadTable("T")
+    val snapshot2 = table.snapshotManager.snapshot(2)
+    val snapshot3 = table.snapshotManager.snapshot(3)
+
+    checkAnswer(
+      spark.sql(
+        s"CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'tag2', 
watermark => ${1000L - 1})"),
+      Row("tag2", 2L, snapshot2.timeMillis, 
String.valueOf(snapshot2.watermark)) :: Nil
+    )
+
+    checkAnswer(
+      spark.sql(
+        s"CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'tag3', 
watermark => ${1000L + 1})"),
+      Row("tag3", 3L, snapshot3.timeMillis, 
String.valueOf(snapshot3.watermark)) :: Nil
+    )
+
+    intercept[SnapshotNotExistException] {
+      spark.sql(
+        s"CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'tag4', 
watermark => ${2000L + 1})")
+    }
+  }
+
+  test("Paimon Procedure: create tags from tagged snapshot watermarks after 
expire") {
+    spark.sql("CREATE TABLE T (k INT, v STRING)")
+
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(1), 
BinaryString.fromString("a")))
+    spark.sql("CALL sys.create_tag(table => 'test.T', tag => 'tag1', snapshot 
=> 1)")
+    writeWithWatermark("T", 2000L, GenericRow.of(Integer.valueOf(2), 
BinaryString.fromString("b")))
+
+    spark.sql("CALL sys.expire_snapshots(table => 'test.T', retain_max => 1, 
retain_min => 1)")
+
+    val table = loadTable("T")
+    assert(!table.snapshotManager.snapshotExists(1))
+
+    val tagSnapshot = table.tagManager.getOrThrow("tag1")
+    val snapshot2 = table.snapshotManager.snapshot(2)
+
+    checkAnswer(
+      spark.sql(
+        s"CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'tag2', 
watermark => ${tagSnapshot.watermark - 1})"),
+      Row("tag2", 1L, tagSnapshot.timeMillis, 
String.valueOf(tagSnapshot.watermark)) :: Nil
+    )
+
+    checkAnswer(
+      spark.sql(
+        s"CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'tag3', 
watermark => ${snapshot2.watermark - 1})"),
+      Row("tag3", 2L, snapshot2.timeMillis, 
String.valueOf(snapshot2.watermark)) :: Nil
+    )
+  }
+
+  test("Paimon Procedure: prefer earlier tagged snapshot when watermarks are 
equal") {
+    spark.sql("CREATE TABLE T (k INT, v STRING)")
+
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(1), 
BinaryString.fromString("a")))
+    spark.sql("CALL sys.create_tag(table => 'test.T', tag => 'keep1', snapshot 
=> 1)")
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(2), 
BinaryString.fromString("b")))
+
+    spark.sql("CALL sys.expire_snapshots(table => 'test.T', retain_max => 1, 
retain_min => 1)")
+
+    val table = loadTable("T")
+    assert(!table.snapshotManager.snapshotExists(1))
+    val tagged = table.tagManager.getOrThrow("keep1")
+
+    checkAnswer(
+      spark.sql(
+        "CALL sys.create_tag_from_watermark(table => 'test.T', tag => 
'tag_from_wm', watermark => 1000)"),
+      Row("tag_from_wm", 1L, tagged.timeMillis, 
String.valueOf(tagged.watermark)) :: Nil
+    )
+  }
+
+  test("Paimon Procedure: select the first live snapshot in an equal-watermark 
interval") {
+    spark.sql("CREATE TABLE T (k INT, v STRING)")
+
+    writeWithWatermark("T", 500L, GenericRow.of(Integer.valueOf(1), 
BinaryString.fromString("a")))
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(2), 
BinaryString.fromString("b")))
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(3), 
BinaryString.fromString("c")))
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(4), 
BinaryString.fromString("d")))
+    writeWithWatermark("T", 2000L, GenericRow.of(Integer.valueOf(5), 
BinaryString.fromString("e")))
+
+    val table = loadTable("T")
+    val snapshot2 = table.snapshotManager.snapshot(2)
+    val snapshot5 = table.snapshotManager.snapshot(5)
+
+    checkAnswer(
+      spark.sql(
+        "CALL sys.create_tag_from_watermark(table => 'test.T', tag => 
'plateau', watermark => 1000)"),
+      Row("plateau", 2L, snapshot2.timeMillis, 
String.valueOf(snapshot2.watermark)) :: Nil
+    )
+
+    checkAnswer(
+      spark.sql(
+        "CALL sys.create_tag_from_watermark(table => 'test.T', tag => 'later', 
watermark => 2000)"),
+      Row("later", 5L, snapshot5.timeMillis, 
String.valueOf(snapshot5.watermark)) :: Nil
+    )
+  }
+
+  test("Paimon Procedure: keep earlier live snapshot when a later tag has the 
same watermark") {
+    spark.sql("CREATE TABLE T (k INT, v STRING)")
+
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(1), 
BinaryString.fromString("a")))
+    writeWithWatermark("T", 1000L, GenericRow.of(Integer.valueOf(2), 
BinaryString.fromString("b")))
+    spark.sql("CALL sys.create_tag(table => 'test.T', tag => 'keep2', snapshot 
=> 2)")
+
+    val table = loadTable("T")
+    val snapshot1 = table.snapshotManager.snapshot(1)
+    assert(table.tagManager.getOrThrow("keep2").id == 2)
+
+    checkAnswer(
+      spark.sql(
+        "CALL sys.create_tag_from_watermark(table => 'test.T', tag => 
'tag_from_wm', watermark => 1000)"),
+      Row("tag_from_wm", 1L, snapshot1.timeMillis, 
String.valueOf(snapshot1.watermark)) :: Nil
+    )
+  }
+
+  private var commitIdentifier = 0L
+
+  private def writeWithWatermark(
+      tableName: String,
+      watermark: java.lang.Long,
+      rows: GenericRow*): Unit = {
+    val table = loadTable(tableName)
+    val writeBuilder = table.newStreamWriteBuilder
+    val writer = writeBuilder.newWrite
+    val commit = writeBuilder.newCommit.asInstanceOf[TableCommitImpl]
+    try {
+      rows.foreach(writer.write)
+      commitIdentifier += 1
+      val committable = new ManifestCommittable(commitIdentifier, watermark)
+      val messages = writer.prepareCommit(true, commitIdentifier)
+      var i = 0
+      while (i < messages.size()) {
+        committable.addFileCommittable(messages.get(i))
+        i += 1
+      }
+      commit.commit(committable)
+    } finally {
+      writer.close()
+      commit.close()
+    }
+  }
+}

Reply via email to