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)
//
===========================================================================