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

xuang7 pushed a commit to branch release/v1.2
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/release/v1.2 by this push:
     new 2bb12d27de fix(file-service, v1.2): reject invalid or duplicate 
dataset renames (#7113)
2bb12d27de is described below

commit 2bb12d27de58ba0167c1c208a7e827fd1d3a06cd
Author: Yicong Huang <[email protected]>
AuthorDate: Thu Jul 30 02:26:11 2026 -0400

    fix(file-service, v1.2): reject invalid or duplicate dataset renames (#7113)
    
    ### What changes were proposed in this PR?
    
    Backport of #6426 to `release/v1.2`, cherry-picked from
    d1ac3a3eaf0d49d7eb57565f040844abed6a711b.
    
    Follows the Direct Backport Push convention; opened as a PR (rather than
    a direct push) as part of a backport-coverage audit for fixes merged to
    `main` since early June that were never labeled for backport.
    
    ### Any related issues, documentation, discussions?
    
    Backport of #6426. Originally linked #6424.
    
    ### How was this PR tested?
    
    Release-branch CI runs once the conflicts are resolved and this PR is
    marked ready for review. The cherry-pick **conflicted** and was
    committed with conflict markers.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Yes — backport prepared with Claude Code (mechanical cherry-pick;
    conflicts left as markers for the original author to resolve; the change
    itself is #6426 by its original author).
    
    Co-authored-by: Kunwoo (Chris) <[email protected]>
---
 .../texera/service/resource/DatasetResource.scala  |  92 ++++++++++++-------
 .../service/resource/DatasetResourceSpec.scala     | 100 +++++++++++++++++++++
 2 files changed, 162 insertions(+), 30 deletions(-)

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 1f8f28a85c..98c37cac13 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
@@ -275,21 +275,16 @@ class DatasetResource extends LazyLogging {
       val isDatasetPublic = request.isDatasetPublic
       val isDatasetDownloadable = request.isDatasetDownloadable
 
-      // validate dataset name
-      try {
-        validateDatasetName(datasetName)
-      } catch {
-        case e: IllegalArgumentException =>
-          throw new BadRequestException(e.getMessage)
-      }
+      validateDatasetName(datasetName)
 
       // Check if a dataset with the same name already exists
-      val existingDatasets = context
-        .selectFrom(DATASET)
-        .where(DATASET.OWNER_UID.eq(uid))
-        .and(DATASET.NAME.eq(datasetName))
-        .fetch()
-      if (!existingDatasets.isEmpty) {
+      val duplicateExists = ctx.fetchExists(
+        ctx
+          .selectFrom(DATASET)
+          .where(DATASET.OWNER_UID.eq(uid))
+          .and(DATASET.NAME.eq(datasetName))
+      )
+      if (duplicateExists) {
         throw new BadRequestException("Dataset with the same name already 
exists")
       }
 
@@ -302,11 +297,13 @@ class DatasetResource extends LazyLogging {
       dataset.setOwnerUid(uid)
 
       // insert record and get created dataset with did
-      val createdDataset = ctx
-        .insertInto(DATASET)
-        .set(ctx.newRecord(DATASET, dataset))
-        .returning()
-        .fetchOne()
+      val createdDataset = failOnDuplicateDatasetName {
+        ctx
+          .insertInto(DATASET)
+          .set(ctx.newRecord(DATASET, dataset))
+          .returning()
+          .fetchOne()
+      }
 
       // Initialize the repository in LakeFS
       val repositoryName = s"dataset-${createdDataset.getDid}"
@@ -510,8 +507,24 @@ class DatasetResource extends LazyLogging {
         throw new ForbiddenException(ERR_USER_HAS_NO_ACCESS_TO_DATASET_MESSAGE)
       }
 
+      validateDatasetName(modificator.name)
+
+      // Check if the owner already has another dataset with the same name
+      val duplicateExists = ctx.fetchExists(
+        ctx
+          .selectFrom(DATASET)
+          .where(DATASET.OWNER_UID.eq(dataset.getOwnerUid))
+          .and(DATASET.NAME.eq(modificator.name))
+          .and(DATASET.DID.notEqual(dataset.getDid))
+      )
+      if (duplicateExists) {
+        throw new BadRequestException("Dataset with the same name already 
exists")
+      }
+
       dataset.setName(modificator.name)
-      datasetDao.update(dataset)
+      failOnDuplicateDatasetName {
+        datasetDao.update(dataset)
+      }
       Response.ok().build()
     }
   }
@@ -1360,29 +1373,48 @@ class DatasetResource extends LazyLogging {
       .fetchInto(classOf[String])
   }
 
+  private val DATASET_NAME_MAX_LENGTH = 128
+  private val DATASET_NAME_PATTERN = "^[A-Za-z0-9_-]+$".r
+
   /**
     * Validates the dataset name.
     *
     * Rules:
-    * - Must be at least 1 character long.
-    * - Only lowercase letters, numbers, underscores, and hyphens are allowed.
-    * - Cannot start with a hyphen.
+    * - Must be 1 to 128 characters long.
+    * - Only letters, numbers, underscores, and hyphens are allowed.
     *
     * @param name The dataset name to validate.
-    * @throws java.lang.IllegalArgumentException if the name is invalid.
+    * @throws jakarta.ws.rs.BadRequestException if the name is invalid.
     */
   private def validateDatasetName(name: String): Unit = {
-    val datasetNamePattern = "^[A-Za-z0-9_-]+$".r
-    if (!datasetNamePattern.matches(name)) {
-      throw new IllegalArgumentException(
-        s"Invalid dataset name: '$name'. " +
-          "Dataset names must be at least 1 character long and " +
-          "contain only lowercase letters, numbers, underscores, and hyphens, 
" +
-          "and cannot start with a hyphen."
+    if (name == null || !DATASET_NAME_PATTERN.matches(name)) {
+      throw new BadRequestException(
+        "Invalid dataset name: only letters, numbers, underscores, and hyphens 
are allowed."
+      )
+    }
+    if (name.length > DATASET_NAME_MAX_LENGTH) {
+      throw new BadRequestException(
+        s"Invalid dataset name: name must be at most $DATASET_NAME_MAX_LENGTH 
characters long."
       )
     }
   }
 
+  /**
+    * Runs a dataset write and translates a (owner_uid, name) unique-constraint
+    * violation into the same BadRequestException the pre-checks throw, so
+    * requests losing a concurrent race get a 400 instead of a 500.
+    */
+  private[resource] def failOnDuplicateDatasetName[T](op: => T): T = {
+    try op
+    catch {
+      case e: DataAccessException =>
+        if (e.sqlState() == "23505") {
+          throw new BadRequestException("Dataset with the same name already 
exists")
+        }
+        throw e
+    }
+  }
+
   private def fetchDatasetVersions(ctx: DSLContext, did: Integer): 
List[DatasetVersion] = {
     ctx
       .selectFrom(DATASET_VERSION)
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 1730d12a0a..2f2840393a 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
@@ -632,6 +632,106 @@ class DatasetResourceSpec
     reloaded.getRepositoryName shouldEqual "rename-keeps-repo-stable"
   }
 
+  it should "refuse to rename dataset to an invalid name" in {
+    val dataset = new Dataset
+    dataset.setName("rename-invalid-src")
+    dataset.setRepositoryName("rename-invalid-src-repo")
+    dataset.setDescription("for rename invalid-name test")
+    dataset.setOwnerUid(ownerUser.getUid)
+    dataset.setIsPublic(true)
+    dataset.setIsDownloadable(true)
+    datasetDao.insert(dataset)
+
+    Seq("", "a/b", "has space", "名前", "dot.dot", "a" * 129, null) foreach { 
invalidName =>
+      withClue(s"renaming to '$invalidName': ") {
+        assertThrows[BadRequestException] {
+          datasetResource.updateDatasetName(
+            DatasetResource.DatasetNameModification(dataset.getDid, 
invalidName),
+            sessionUser
+          )
+        }
+      }
+    }
+
+    datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual 
"rename-invalid-src"
+  }
+
+  it should "refuse to rename dataset to a name already used by another 
dataset of the same owner" in {
+    val existing = new Dataset
+    existing.setName("rename-dup-existing")
+    existing.setRepositoryName("rename-dup-existing-repo")
+    existing.setDescription("existing dataset for duplicate rename test")
+    existing.setOwnerUid(ownerUser.getUid)
+    existing.setIsPublic(true)
+    existing.setIsDownloadable(true)
+    datasetDao.insert(existing)
+
+    val renamed = new Dataset
+    renamed.setName("rename-dup-source")
+    renamed.setRepositoryName("rename-dup-source-repo")
+    renamed.setDescription("dataset being renamed in duplicate rename test")
+    renamed.setOwnerUid(ownerUser.getUid)
+    renamed.setIsPublic(true)
+    renamed.setIsDownloadable(true)
+    datasetDao.insert(renamed)
+
+    assertThrows[BadRequestException] {
+      datasetResource.updateDatasetName(
+        DatasetResource.DatasetNameModification(renamed.getDid, 
"rename-dup-existing"),
+        sessionUser
+      )
+    }
+
+    datasetDao.fetchOneByDid(renamed.getDid).getName shouldEqual 
"rename-dup-source"
+  }
+
+  it should "allow renaming a dataset to its own current name" in {
+    val dataset = new Dataset
+    dataset.setName("rename-self-noop")
+    dataset.setRepositoryName("rename-self-noop-repo")
+    dataset.setDescription("for rename-to-self test")
+    dataset.setOwnerUid(ownerUser.getUid)
+    dataset.setIsPublic(true)
+    dataset.setIsDownloadable(true)
+    datasetDao.insert(dataset)
+
+    val response = datasetResource.updateDatasetName(
+      DatasetResource.DatasetNameModification(dataset.getDid, 
"rename-self-noop"),
+      sessionUser
+    )
+
+    response.getStatus shouldEqual 200
+    datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual 
"rename-self-noop"
+  }
+
+  it should "allow renaming to a name used by a dataset of a different owner" 
in {
+    val otherOwners = new Dataset
+    otherOwners.setName("rename-cross-owner")
+    otherOwners.setRepositoryName("rename-cross-owner-repo")
+    otherOwners.setDescription("other owner's dataset for cross-owner rename 
test")
+    otherOwners.setOwnerUid(otherAdminUser.getUid)
+    otherOwners.setIsPublic(true)
+    otherOwners.setIsDownloadable(true)
+    datasetDao.insert(otherOwners)
+
+    val dataset = new Dataset
+    dataset.setName("rename-cross-source")
+    dataset.setRepositoryName("rename-cross-source-repo")
+    dataset.setDescription("dataset being renamed in cross-owner rename test")
+    dataset.setOwnerUid(ownerUser.getUid)
+    dataset.setIsPublic(true)
+    dataset.setIsDownloadable(true)
+    datasetDao.insert(dataset)
+
+    val response = datasetResource.updateDatasetName(
+      DatasetResource.DatasetNameModification(dataset.getDid, 
"rename-cross-owner"),
+      sessionUser
+    )
+
+    response.getStatus shouldEqual 200
+    datasetDao.fetchOneByDid(dataset.getDid).getName shouldEqual 
"rename-cross-owner"
+  }
+
   // 
===========================================================================
   // Multipart upload tests (merged in)
   // 
===========================================================================

Reply via email to