This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new ea0fc10cd8 fix(file-service): reject over-long dataset version names 
(#8875)
ea0fc10cd8 is described below

commit ea0fc10cd8478b3b7368910680bede73a3246e73
Author: Kunwoo (Chris) <[email protected]>
AuthorDate: Sat Oct 10 21:59:26 2026 +0000

    fix(file-service): reject over-long dataset version names (#8875)
    
    ### What changes were proposed in this PR?
    
    Root cause. `createDatasetVersion` builds the name `v{N} -
    {description}` and commits to LakeFS first, then inserts the
    `dataset_version` row (`DatasetResource.scala`). `dataset_version.name`
    is `VARCHAR(128)`, so a long description makes the insert fail after the
    commit, and the commit is outside the DB transaction. The request
    returns a 500, the staged files are already committed with no version
    row, and the dataset page then shows neither a pending change nor a
    version until the next version is created.
    
    Fix. `createDatasetVersion` now rejects a name longer than 128
    characters with a 400 before it creates the LakeFS commit, using the
    same check `createModelVersion` already has. The length is counted in
    code points rather than UTF-16 code units, since `VARCHAR(n)` counts
    characters and an emoji would otherwise be rejected early. The staged
    files stay pending, so the user can shorten the description and submit
    again. The frontend already shows the message from the response, so it
    needs no change.
    
    ```
    Before:  Submit -> LakeFS commit -> INSERT dataset_version fails -> 500, 
files committed, no version row
    After:   Submit -> name over 128 characters -> 400 "Version name is too 
long: ..." -> files stay pending
    ```
    
    ### Any related issues, documentation, discussions?
    
    Closes #8873
    
    ### How was this PR tested?
    
    Added five tests to `DatasetResourceSpec`: version naming (null, empty
    and non-empty description), a name of exactly 128 characters (accepted),
    a name of 129 characters (rejected with a 400, nothing committed, and a
    retry succeeds), a 128-character name made of emoji (accepted) and a
    129-character one (rejected), and the existing no-staged-changes 400.
    Before the fix, the over-long test failed with
    `org.jooq.exception.DataException` from the insert. A first version of
    the guard that used `String.length` failed the emoji test, which is why
    the length is counted in code points.
    
    `sbt "FileService/testOnly *DatasetResourceSpec"` gives 157 passed, 0
    failed, and `scalafixAll` and `scalafmtAll` change nothing. This ran
    locally with a MinIO container in place of the RustFS image, which could
    not be pulled on that machine, so CI is the run against RustFS. It was
    not exercised through the UI.
    
    
    
https://github.com/user-attachments/assets/f3e5d22d-89fb-477a-88a0-37f77c4e09c7
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code, Claude Sonnet 5.5
---
 .../texera/service/resource/DatasetResource.scala  | 14 ++++
 .../service/resource/DatasetResourceSpec.scala     | 94 ++++++++++++++++++++++
 2 files changed, 108 insertions(+)

diff --git 
a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala
 
b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala
index 76eb3ebd74..4da407fd31 100644
--- 
a/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala
+++ 
b/file-service/src/main/scala/org/apache/texera/service/resource/DatasetResource.scala
@@ -270,6 +270,9 @@ class DatasetResource extends LazyLogging {
   // varchar(246) on DBs upgraded through sql/updates/18.sql, narrower than 
model's.
   private val COVER_IMAGE_MAX_PATH_LENGTH = 246
 
+  // Matches dataset_version.name VARCHAR(128).
+  private val MAX_VERSION_NAME_LENGTH = 128
+
   private val resourceType = ResourceType.Dataset
 
   /**
@@ -450,6 +453,17 @@ class DatasetResource extends LazyLogging {
         s"v${versionCount + 1} - $sanitizedVersionName"
       }
 
+      // Before the commit: the commit is outside this transaction, so a name 
the insert
+      // rejects would leave a commit no version points at and strand the 
staged files.
+      // Code points, not String.length: VARCHAR(n) counts characters, so an 
emoji counts once.
+      val newVersionNameLength = newVersionName.codePointCount(0, 
newVersionName.length)
+      if (newVersionNameLength > MAX_VERSION_NAME_LENGTH) {
+        throw new BadRequestException(
+          s"Version name is too long: $newVersionNameLength characters, " +
+            s"maximum is $MAX_VERSION_NAME_LENGTH."
+        )
+      }
+
       // Create a commit in LakeFS
       val commit = withLakeFSErrorHandling {
         LakeFSStorageClient.createCommit(
diff --git 
a/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
 
b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
index b728b748c5..db4c2437eb 100644
--- 
a/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
+++ 
b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
@@ -3881,4 +3881,98 @@ class DatasetResourceSpec
       .get
       .getFilePath should startWith("/dataset/")
   }
+
+  // 
---------------------------------------------------------------------------
+  // createDatasetVersion
+  // 
---------------------------------------------------------------------------
+
+  // The state right after a user drops a file on "Create New Version": a 
dataset whose
+  // repository holds one staged (uncommitted) file and no version row yet.
+  private def seedDatasetWithStagedFile(prefix: String): Dataset = {
+    val dataset = seedDataset(s"$prefix-${System.nanoTime()}", 
ownerUser.getUid)
+    LakeFSStorageClient.initRepo(dataset.getRepositoryName)
+    stageFile(dataset, "staged.txt")
+    dataset
+  }
+
+  private def stageFile(dataset: Dataset, path: String): Unit =
+    LakeFSStorageClient.writeFileToRepo(
+      dataset.getRepositoryName,
+      path,
+      new ByteArrayInputStream("content".getBytes(StandardCharsets.UTF_8))
+    )
+
+  private def createVersion(
+      dataset: Dataset,
+      description: String
+  ): DatasetResource.DashboardDatasetVersion =
+    datasetResource.createDatasetVersion(description, dataset.getDid, 
sessionUser)
+
+  // "v1 - " takes 5 of the 128 characters dataset_version.name allows.
+  private val maxDescriptionLength = 128 - "v1 - ".length
+
+  "createDatasetVersion" should "name versions by their number, followed by 
the description when given" in {
+    val dataset = seedDatasetWithStagedFile("version-naming")
+
+    createVersion(dataset, null).datasetVersion.getName shouldBe "v1"
+
+    stageFile(dataset, "second.txt")
+    createVersion(dataset, "second upload").datasetVersion.getName shouldBe 
"v2 - second upload"
+
+    stageFile(dataset, "third.txt")
+    createVersion(dataset, "").datasetVersion.getName shouldBe "v3"
+  }
+
+  it should "accept a description that makes the version name exactly 128 
characters" in {
+    val dataset = seedDatasetWithStagedFile("version-name-at-limit")
+
+    val created = createVersion(dataset, "d" * maxDescriptionLength)
+
+    created.datasetVersion.getName should have length 128
+  }
+
+  it should "reject a description that pushes the version name past 128 
characters without committing" in {
+    val dataset = seedDatasetWithStagedFile("version-name-too-long")
+
+    val ex = intercept[WebApplicationException] {
+      createVersion(dataset, "d" * (maxDescriptionLength + 1))
+    }
+    assertStatus(ex, 400)
+    ex.getMessage should include("too long")
+
+    // Nothing was committed: the file is still staged, so the dataset page 
keeps showing it
+    // as a pending change.
+    LakeFSStorageClient
+      .retrieveUncommittedObjects(dataset.getRepositoryName)
+      .map(_.getPath) shouldBe List("staged.txt")
+
+    // The user can fix the description and retry; no version row was created, 
so it is still v1.
+    createVersion(dataset, "shorter").datasetVersion.getName shouldBe "v1 - 
shorter"
+  }
+
+  it should "count characters rather than UTF-16 code units against the 128 
limit" in {
+    val dataset = seedDatasetWithStagedFile("version-name-unicode")
+    // U+1F600 is one character but two UTF-16 code units.
+    val emoji = new String(Character.toChars(0x1f600))
+
+    val ex = intercept[WebApplicationException] {
+      createVersion(dataset, emoji * (maxDescriptionLength + 1))
+    }
+    assertStatus(ex, 400)
+    ex.getMessage should include("129 characters")
+
+    val name = createVersion(dataset, emoji * 
maxDescriptionLength).datasetVersion.getName
+    name.codePointCount(0, name.length) shouldBe 128
+  }
+
+  it should "reject a version when there are no staged changes" in {
+    val dataset = seedDatasetWithStagedFile("version-no-changes")
+    createVersion(dataset, "first")
+
+    val ex = intercept[WebApplicationException] {
+      createVersion(dataset, "empty")
+    }
+    assertStatus(ex, 400)
+    datasetResource.getDatasetVersionList(dataset.getDid, sessionUser) should 
have size 1
+  }
 }

Reply via email to