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 bc94796189 feat(storage): singularize the resource-type prefix on
logical paths (#7789)
bc94796189 is described below
commit bc9479618953a24b057b4e7ce4d13e9af71775a2
Author: Tanishq Gandhi <[email protected]>
AuthorDate: Fri Aug 21 00:31:39 2026 +0000
feat(storage): singularize the resource-type prefix on logical paths (#7789)
### What changes were proposed in this PR?
The leading segment of a versioned-resource logical path was plural
while every name it refers to is singular — the backing tables
(`dataset`, `model`), the dashboard search's resource type, and the
hub's entity type. This makes the
prefix match them.
```
Before: /datasets/[email protected]/twitter/v1/f.csv table: dataset
/models/[email protected]/resnet/v1/w.pt table: model
After: /dataset/[email protected]/twitter/v1/f.csv table: dataset
/model/[email protected]/resnet/v1/w.pt table: model
```
| Layer | Change |
| --- | --- |
| Enums | `ResourceType` in Scala (`Datasets`/`Models` →
`Dataset`/`Model`), TypeScript (`Datasets` → `Dataset`), Python
(`DATASETS` → `DATASET`) |
| Call sites | `FileResolver`, `FileListerSourceOpExec`,
`DocumentFactory`, `LakeFSFileDocument`, `DatasetResource`,
`DatasetFileNode`, `dataset-selection-modal`, `dataset_file_document` |
| Migration | `36.sql` restored as it shipped; normalization added in a
new `39.sql` |
| Examples | the two shipped `bin/single-node/examples/workflows/*.json`
carried the plural prefix |
Two hardcoded `"dataset"` string literals on the DB side now read from
the enum, since the prefix and the type name are the same word again:
- `SearchQueryBuilder.DATASET_RESOURCE_TYPE`, inlined into the unified
search SQL
- `EntityType.Dataset.value`, written to `user_action.resource_type`
**On the migration.** `36.sql` introduced the prefix in its plural form
and is restored here byte-for-byte, so its checksum stays intact; the
normalization lives in a new `39.sql` (38 is taken). Correcting 36 in
place could not reach the databases that need it: liquibase selects
changeSets by id — `bin/local-dev/main.sh:1824` skips on a match in
`databasechangelog` without reading the file — so the databases holding
plural paths are exactly the ones that recorded id 36 and will never run
it again, and `MD5SUM` is left NULL by design, so nothing reports a
mismatch and the stale paths surface only when someone opens one of
those workflows.
`39.sql` normalizes both shapes, so it converges whether or not 36 ran:
| Stored value | Action |
| --- | --- |
| `/[email protected]/ds/v1/f.csv` (unprefixed legacy) | prepend `dataset` |
| `/datasets/[email protected]/ds/v1/f.csv` (plural prefix) | rewrite the
leading segment |
| `/dataset/...` | already current — untouched |
Both cases apply only when the path's owner and name segments match a
real `(user.email, dataset.name)` pair — read at parts 1 and 2
unprefixed, 2 and 3 prefixed. That guard is what keeps the migration off
the plain filesystem paths and URLs this column also holds: `/datasets`
is an ordinary directory name, and `FileResolver` tries
`localResolveFunc` first, so a `fileName` of `/datasets/imdb/movies.csv`
can be a working local mount and is left alone. A path naming a dataset
that no longer exists therefore keeps its old form; it cannot resolve
either way. `/models/` is out of scope, since `37.sql` creates that
table and runs after this changeset.
### Any related issues, documentation, discussions?
Additional work on #6495, which introduced the resource-type prefix.
### How was this PR tested?
Existing suites, updated where they pin the path form.
```bash
sbt "WorkflowCore/testOnly org.apache.texera.amber.storage.FileResolverSpec
org.apache.texera.amber.core.storage.DocumentFactorySpec
org.apache.texera.amber.core.storage.model.LakeFSFileDocumentSpec" \
"WorkflowOperator/testOnly
org.apache.texera.amber.operator.source.dataset.FileListerSourceOpExecSpec" \
"FileService/testOnly
org.apache.texera.service.type.DatasetFileNodeSpec" \
"WorkflowExecutionService/testOnly
org.apache.texera.web.resource.dashboard.DatasetSearchQueryBuilderSpec
org.apache.texera.web.resource.dashboard.DashboardResourceSpec
org.apache.texera.web.resource.dashboard.UnifiedResourceSchemaSpec
org.apache.texera.web.resource.dashboard.hub.HubResourceSpec
org.apache.texera.web.resource.dashboard.user.workflow.WorkflowExecutionsResourceSpec
org.apache.texera.web.resource.dashboard.file.DatasetResourceSpec"
```
```bash
pytest amber/src/test/python/pytexera/storage/test_dataset_file_document.py
```
```bash
cd frontend && yarn ng test --watch=false
--include='**/datasetVersionFileTree.spec.ts'
--include='**/dataset-selection-modal.component.spec.ts'
--include='**/dataset-version-selector.component.spec.ts'
--include='**/user-dataset-version-filetree.component.spec.ts'
--include='**/dataset-detail.component.spec.ts'
```
`sbt Test/compile` and `tsc` on both app and spec sources are clean, so
the enum rename has no missed call sites.
`39.sql` was run against a seeded Postgres over 13 fixtures, one per
case: legacy unprefixed, plural prefix, `/datasets`-rooted local mounts
(3-segment, 4-segment, and as `datasetVersionPath`), plural naming a
deleted dataset, `/models/`, already-singular, a URL, `/datasets/`
appearing mid-path, non-array `operators`, absent property, and a row
checking other operator properties, links and operator order survive.
Correct in every case, on Postgres 18 and 15.11, and idempotent — a
second run reports 0 rows.
Tested through liquibase's id tracking rather than only by piping the
SQL in, emulating `main.sh` against a real `databasechangelog` table:
```
fresh DB: changeSet 36 ran, 39 rewrote ->
/dataset/[email protected]/ds/v1/a.csv
recorded id 36: changeSet 36 skipped, 39 rewrote ->
/dataset/[email protected]/ds/v1/a.csv
```
Also covered: `39` alone with no `36`, `39` twice, and `39` before `36`
out of order — all four converge on the same state, with the local mount
untouched throughout.
The same statements were also run inside a Kubernetes deployment:
`bin/k8s` rendered with `values-development.yaml`, the postgresql
resources applied to minikube, and the SQL piped into the pod against
the real 39-table schema the chart bootstraps. Row-for-row identical to
local, including the local-mount cases staying untouched. Note that the
chart's postgres init runs only `texera_ddl.sql` and the three catalog
scripts, and `texera_db` has no `databasechangelog` table — no liquibase
changeSet executes on a Kubernetes deployment.
`sbt scalafmtCheckAll`, `ruff check`, `ruff format --check`, and
`prettier --check`
are clean.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 5)
---------
Co-authored-by: Claude Opus 5 <[email protected]>
Co-authored-by: ali <[email protected]>
---
.../pytexera/storage/dataset_file_document.py | 10 +-
.../main/python/pytexera/storage/resource_type.py | 2 +-
.../resource/dashboard/SearchQueryBuilder.scala | 11 +-
.../web/resource/dashboard/hub/EntityType.scala | 3 +-
.../pytexera/storage/test_dataset_file_document.py | 34 ++-
.../workflow/WorkflowExecutionsResourceSpec.scala | 6 +-
...xample] Data Exploration on Movies Dataset.json | 2 +-
...[Example] Machine Learning on Iris Dataset.json | 2 +-
.../amber/core/storage/DocumentFactory.scala | 8 +-
.../texera/amber/core/storage/FileResolver.scala | 16 +-
.../texera/amber/core/storage/ResourceType.scala | 4 +-
.../core/storage/model/LakeFSFileDocument.scala | 4 +-
.../amber/core/storage/DocumentFactorySpec.scala | 8 +-
.../storage/model/LakeFSFileDocumentSpec.scala | 10 +-
.../texera/amber/storage/FileResolverSpec.scala | 24 +-
.../source/dataset/FileListerSourceOpExec.scala | 6 +-
.../dataset/FileListerSourceOpExecSpec.scala | 26 +--
.../texera/service/resource/DatasetResource.scala | 2 +-
.../service/type/dataset/DatasetFileNode.scala | 16 +-
.../service/type/dataset/DatasetFileNodeSpec.scala | 12 +-
.../app/common/type/datasetVersionFileTree.spec.ts | 8 +-
.../src/app/common/type/datasetVersionFileTree.ts | 2 +-
frontend/src/app/common/type/resource-type.ts | 4 +-
.../dataset-detail.component.spec.ts | 4 +-
...user-dataset-version-filetree.component.spec.ts | 6 +-
.../dataset-selection-modal.component.spec.ts | 8 +-
.../dataset-selection-modal.component.ts | 4 +-
.../dataset-version-selector.component.spec.ts | 8 +-
sql/changelog.xml | 5 +
sql/updates/39.sql | 254 +++++++++++++++++++++
30 files changed, 388 insertions(+), 121 deletions(-)
diff --git a/amber/src/main/python/pytexera/storage/dataset_file_document.py
b/amber/src/main/python/pytexera/storage/dataset_file_document.py
index b8d778d342..2d14f858e4 100644
--- a/amber/src/main/python/pytexera/storage/dataset_file_document.py
+++ b/amber/src/main/python/pytexera/storage/dataset_file_document.py
@@ -59,18 +59,18 @@ class DatasetFileDocument:
:param file_path:
Expected format -
- "/datasets/ownerEmail/datasetName/versionName/fileRelativePath"
+ "/dataset/ownerEmail/datasetName/versionName/fileRelativePath"
Example:
- "/datasets/[email protected]/twitterDataset/v1/california/tw1.csv"
+ "/dataset/[email protected]/twitterDataset/v1/california/tw1.csv"
"""
parts = file_path.strip("/").split("/")
invalid_format = ValueError(
"Invalid file path format. Expected: "
- "/datasets/ownerEmail/datasetName/versionName/fileRelativePath"
+ "/dataset/ownerEmail/datasetName/versionName/fileRelativePath"
)
- # TODO(datasets-prefix): require the prefix once all stored paths are
migrated (36.sql) and ml model support work is completed.
+ # TODO(dataset-prefix): require the prefix once all stored paths are
migrated (36.sql) and ml model support work is completed.
if parts and parts[0] in {t.value for t in ResourceType}:
if len(parts) < 5:
raise invalid_format
@@ -80,7 +80,7 @@ class DatasetFileDocument:
self.version_name = parts[3]
self.file_relative_path = "/".join(parts[4:])
elif len(parts) >= 4:
- self.resource_type = ResourceType.DATASETS
+ self.resource_type = ResourceType.DATASET
self.owner_email = parts[0]
self.dataset_name = parts[1]
self.version_name = parts[2]
diff --git a/amber/src/main/python/pytexera/storage/resource_type.py
b/amber/src/main/python/pytexera/storage/resource_type.py
index 596fb5d745..e77ec9dd49 100644
--- a/amber/src/main/python/pytexera/storage/resource_type.py
+++ b/amber/src/main/python/pytexera/storage/resource_type.py
@@ -21,4 +21,4 @@ from enum import Enum
class ResourceType(str, Enum):
"""The leading segment of a logical file path, identifying the resource
kind"""
- DATASETS = "datasets"
+ DATASET = "dataset"
diff --git
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
index e19755e611..e8a651cb82 100644
---
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
+++
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/SearchQueryBuilder.scala
@@ -19,6 +19,7 @@
package org.apache.texera.web.resource.dashboard
+import org.apache.texera.amber.core.storage.ResourceType
import org.apache.texera.dao.SqlServer
import org.apache.texera.web.resource.dashboard.DashboardResource.{
DashboardClickableFileEntry,
@@ -33,10 +34,18 @@ object SearchQueryBuilder {
SqlServer
.getInstance()
.createDSLContext()
+ // Resource-type discriminators for dashboard search. They are inlined into
the search SQL,
+ // pattern-matched in DashboardResource, and duplicated as bare literals in
the frontend.
+ // DATASET_RESOURCE_TYPE aliases ResourceType.Dataset, whose value doubles
as the leading
+ // segment of a storage logical path, so renaming that prefix silently
changes search results
+ // with no compile error here.
+ // TODO: give every resource type (file, workflow, project, dataset, model)
a single shared
+ // constant/enum instead of these raw strings, so the search contract
has one source of
+ // truth and no longer borrows an unrelated storage constant.
val FILE_RESOURCE_TYPE = "file"
val WORKFLOW_RESOURCE_TYPE = "workflow"
val PROJECT_RESOURCE_TYPE = "project"
- val DATASET_RESOURCE_TYPE = "dataset"
+ val DATASET_RESOURCE_TYPE = ResourceType.Dataset.toString
val ALL_RESOURCE_TYPE = ""
}
diff --git
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/EntityType.scala
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/EntityType.scala
index 0cf1bd4cc8..bd83b7837e 100644
---
a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/EntityType.scala
+++
b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/hub/EntityType.scala
@@ -20,6 +20,7 @@
package org.apache.texera.web.resource.dashboard.hub
import com.fasterxml.jackson.annotation.{JsonCreator, JsonValue}
+import org.apache.texera.amber.core.storage.ResourceType
/**
* Defines all supported entity types for Hub resources.
@@ -34,7 +35,7 @@ sealed trait EntityType {
object EntityType {
case object Workflow extends EntityType { val value = "workflow" }
- case object Dataset extends EntityType { val value = "dataset" }
+ case object Dataset extends EntityType { val value: String =
ResourceType.Dataset.toString }
private val values = Seq(Workflow, Dataset)
diff --git
a/amber/src/test/python/pytexera/storage/test_dataset_file_document.py
b/amber/src/test/python/pytexera/storage/test_dataset_file_document.py
index bef134bc74..b90dcce81b 100644
--- a/amber/src/test/python/pytexera/storage/test_dataset_file_document.py
+++ b/amber/src/test/python/pytexera/storage/test_dataset_file_document.py
@@ -47,18 +47,18 @@ def make_response(status_code: int, body=None, content:
bytes = b""):
class TestDatasetFileDocumentInit:
def test_parses_prefixed_path(self, auth_env):
- doc = DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ doc = DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
assert doc.owner_email == "[email protected]"
assert doc.dataset_name == "ds"
assert doc.version_name == "v1"
assert doc.file_relative_path == "file.csv"
def test_joins_nested_relative_path_back_with_slashes(self, auth_env):
- doc = DatasetFileDocument("/datasets/[email protected]/ds/v1/a/b/c/file.csv")
+ doc = DatasetFileDocument("/dataset/[email protected]/ds/v1/a/b/c/file.csv")
assert doc.file_relative_path == "a/b/c/file.csv"
def test_strips_leading_and_trailing_slashes_before_parsing(self,
auth_env):
- doc = DatasetFileDocument("///datasets/[email protected]/ds/v1/file.csv///")
+ doc = DatasetFileDocument("///dataset/[email protected]/ds/v1/file.csv///")
assert doc.owner_email == "[email protected]"
assert doc.file_relative_path == "file.csv"
@@ -80,7 +80,7 @@ class TestDatasetFileDocumentInit:
def test_rejects_prefixed_path_with_too_few_segments(self, auth_env):
with pytest.raises(ValueError, match="Invalid file path format"):
- DatasetFileDocument("/datasets/[email protected]/ds/v1")
+ DatasetFileDocument("/dataset/[email protected]/ds/v1")
def test_rejects_legacy_path_with_too_few_segments(self, auth_env):
with pytest.raises(ValueError, match="Invalid file path format"):
@@ -92,29 +92,29 @@ class TestDatasetFileDocumentInit:
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
)
with pytest.raises(ValueError, match="JWT token is required"):
- DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
def test_treats_empty_jwt_as_missing(self, monkeypatch):
# An empty string is falsy and should be rejected just like an unset
var.
monkeypatch.setenv("USER_JWT_TOKEN", "")
with pytest.raises(ValueError, match="JWT token is required"):
- DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
def test_falls_back_to_default_endpoint_when_env_missing(self,
monkeypatch):
monkeypatch.setenv("USER_JWT_TOKEN", "tok")
monkeypatch.delenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", raising=False
)
- doc = DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ doc = DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
assert doc.presign_endpoint == DEFAULT_ENDPOINT
def test_uses_explicit_endpoint_from_environment(self, auth_env):
- doc = DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ doc = DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
assert doc.presign_endpoint == CUSTOM_ENDPOINT
class TestGetPresignedUrl:
- def _make_doc(self, monkeypatch,
path="/datasets/[email protected]/ds/v1/file.csv"):
+ def _make_doc(self, monkeypatch,
path="/dataset/[email protected]/ds/v1/file.csv"):
monkeypatch.setenv("USER_JWT_TOKEN", "test-jwt-token")
monkeypatch.setenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
@@ -144,9 +144,7 @@ class TestGetPresignedUrl:
def test_url_encodes_filepath_query_parameter(self, monkeypatch):
# urllib.parse.quote keeps "/" as safe by default, but encodes "@"
# and " " — pin both pieces so the contract is explicit.
- doc = self._make_doc(
- monkeypatch, path="/datasets/[email protected]/ds/v1/data file.csv"
- )
+ doc = self._make_doc(monkeypatch, path="/dataset/[email protected]/ds/v1/data
file.csv")
with patch(
"pytexera.storage.dataset_file_document.requests.Session.get"
) as mock_get:
@@ -158,16 +156,16 @@ class TestGetPresignedUrl:
assert "bob%40x.com" in file_path
assert file_path.startswith("/")
- def test_sends_datasets_prefixed_filepath(self, monkeypatch):
- # The reconstructed filePath sent to the file-service carries the
"datasets" prefix.
- doc = self._make_doc(monkeypatch,
path="/datasets/[email protected]/ds/v1/file.csv")
+ def test_sends_dataset_prefixed_filepath(self, monkeypatch):
+ # The reconstructed filePath sent to the file-service carries the
"dataset" prefix.
+ doc = self._make_doc(monkeypatch,
path="/dataset/[email protected]/ds/v1/file.csv")
with patch(
"pytexera.storage.dataset_file_document.requests.Session.get"
) as mock_get:
mock_get.return_value = make_response(200, body={"presignedUrl":
"u"})
doc.get_presigned_url()
_, kwargs = mock_get.call_args
- assert kwargs["params"]["filePath"].startswith("/datasets/")
+ assert kwargs["params"]["filePath"].startswith("/dataset/")
def test_calls_configured_endpoint(self, monkeypatch):
doc = self._make_doc(monkeypatch)
@@ -235,7 +233,7 @@ class TestReadFile:
monkeypatch.setenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
)
- return DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ return DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
def test_returns_bytesio_with_downloaded_content(self, monkeypatch):
doc = self._make_doc(monkeypatch)
@@ -291,7 +289,7 @@ class TestTimeoutsAndRetries:
monkeypatch.setenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
)
- return DatasetFileDocument("/datasets/[email protected]/ds/v1/file.csv")
+ return DatasetFileDocument("/dataset/[email protected]/ds/v1/file.csv")
def test_presigned_url_request_passes_request_timeout(self, monkeypatch):
doc = self._make_doc(monkeypatch)
diff --git
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala
index 7dc5742a18..7cc549004d 100644
---
a/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala
+++
b/amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowExecutionsResourceSpec.scala
@@ -910,7 +910,7 @@ class WorkflowExecutionsResourceSpec
val content =
"""{
| "operators": [
- | {"operatorID": "scanA", "operatorProperties": {"fileName":
"/datasets/[email protected]/LockedDS/v1/data.csv"}},
+ | {"operatorID": "scanA", "operatorProperties": {"fileName":
"/dataset/[email protected]/LockedDS/v1/data.csv"}},
| {"operatorID": "downstreamB", "operatorProperties": {}}
| ],
| "links": [
@@ -969,7 +969,7 @@ class WorkflowExecutionsResourceSpec
val content =
"""{
| "operators": [
- | {"operatorID": "scan", "operatorProperties": {"fileName":
"/datasets/[email protected]/MyDS/v1/data.csv"}}
+ | {"operatorID": "scan", "operatorProperties": {"fileName":
"/dataset/[email protected]/MyDS/v1/data.csv"}}
| ],
| "links": []
|}""".stripMargin
@@ -1408,7 +1408,7 @@ class WorkflowExecutionsResourceSpec
private def scanOperator(operatorId: String, datasetName: String): String =
s"""{"operatorID": "$operatorId", "operatorProperties": """ +
- s"""{"fileName":
"/datasets/$foreignOwnerEmail/$datasetName/v1/data.csv"}}"""
+ s"""{"fileName":
"/dataset/$foreignOwnerEmail/$datasetName/v1/data.csv"}}"""
// Seeds three datasets owned by somebody other than testUser and wires the
workflow as
//
diff --git a/bin/single-node/examples/workflows/[Example] Data Exploration on
Movies Dataset.json b/bin/single-node/examples/workflows/[Example] Data
Exploration on Movies Dataset.json
index cdc9a0e870..5df44f5382 100644
--- a/bin/single-node/examples/workflows/[Example] Data Exploration on Movies
Dataset.json
+++ b/bin/single-node/examples/workflows/[Example] Data Exploration on Movies
Dataset.json
@@ -8,7 +8,7 @@
"fileEncoding": "UTF_8",
"customDelimiter": ",",
"hasHeader": true,
- "fileName":
"/datasets/texera/popular-movies-of-imdb/v1/TMDb_updated.csv",
+ "fileName":
"/dataset/texera/popular-movies-of-imdb/v1/TMDb_updated.csv",
"offset": 0,
"limit": 1000
},
diff --git a/bin/single-node/examples/workflows/[Example] Machine Learning on
Iris Dataset.json b/bin/single-node/examples/workflows/[Example] Machine
Learning on Iris Dataset.json
index 7e9a5d11fd..e4cdc3a8af 100644
--- a/bin/single-node/examples/workflows/[Example] Machine Learning on Iris
Dataset.json
+++ b/bin/single-node/examples/workflows/[Example] Machine Learning on Iris
Dataset.json
@@ -8,7 +8,7 @@
"fileEncoding": "UTF_8",
"customDelimiter": ",",
"hasHeader": true,
- "fileName": "/datasets/texera/iris-species/v1/Iris.csv"
+ "fileName": "/dataset/texera/iris-species/v1/Iris.csv"
},
"inputPorts": [],
"outputPorts": [
diff --git
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
index 8481906c9c..4915d57897 100644
---
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
+++
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/DocumentFactory.scala
@@ -60,8 +60,8 @@ object DocumentFactory {
*/
def openReadonlyDocument(fileUri: URI): ReadonlyVirtualDocument[_] = {
fileUri.getScheme match {
- case DATASET_FILE_URI_SCHEME => new LakeFSFileDocument(fileUri,
ResourceType.Datasets)
- case MODEL_FILE_URI_SCHEME => new LakeFSFileDocument(fileUri,
ResourceType.Models)
+ case DATASET_FILE_URI_SCHEME => new LakeFSFileDocument(fileUri,
ResourceType.Dataset)
+ case MODEL_FILE_URI_SCHEME => new LakeFSFileDocument(fileUri,
ResourceType.Model)
case "file" => new ReadonlyLocalFileDocument(fileUri)
case unsupportedScheme =>
throw new UnsupportedOperationException(
@@ -184,8 +184,8 @@ object DocumentFactory {
*/
def openDocument(uri: URI): (VirtualDocument[_], Option[Schema]) = {
uri.getScheme match {
- case DATASET_FILE_URI_SCHEME => (new LakeFSFileDocument(uri,
ResourceType.Datasets), None)
- case MODEL_FILE_URI_SCHEME => (new LakeFSFileDocument(uri,
ResourceType.Models), None)
+ case DATASET_FILE_URI_SCHEME => (new LakeFSFileDocument(uri,
ResourceType.Dataset), None)
+ case MODEL_FILE_URI_SCHEME => (new LakeFSFileDocument(uri,
ResourceType.Model), None)
case VFS_FILE_URI_SCHEME =>
val IcebergLocation(warehouse, namespace, storageKey) =
resolveIcebergLocation(uri)
diff --git
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
index 1a2e4fcf2a..95127e234d 100644
---
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
+++
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/FileResolver.scala
@@ -93,7 +93,7 @@ object FileResolver {
* caller can dispatch to the right backing table.
* - Legacy unprefixed (datasets only, backward compat):
/ownerEmail/datasetName/versionName/
* fileRelativePath (>= 4 segments), resolved as a dataset. Models are
new and always require
- * the /models/ prefix.
+ * the /model/ prefix.
*
* @param fileName the file path to parse
* @return Some((resourceType, ownerEmail, resourceName, versionName,
fileRelativePath)) if valid,
@@ -115,11 +115,11 @@ object FileResolver {
)
case None =>
// Legacy unprefixed dataset path (backward compat):
/ownerEmail/datasetName/versionName/<file>.
- // TODO(datasets-prefix): require the prefix once all stored paths are
migrated (36.sql).
+ // TODO(dataset-prefix): require the prefix once all stored paths are
migrated (36.sql).
if (pathSegments.length >= 4)
Some(
(
- ResourceType.Datasets,
+ ResourceType.Dataset,
pathSegments(0),
pathSegments(1),
pathSegments(2),
@@ -139,8 +139,8 @@ object FileResolver {
*
* Input: /<prefix>/ownerEmail/resourceName/versionName/fileRelativePath
(or legacy unprefixed)
* Output: {scheme}:///{repositoryName}/{versionHash}/fileRelativePath
- * e.g. /datasets/[email protected]/twitter/v1/dir/f.csv ->
dataset:///dataset-15/adeq233td/dir/f.csv
- * /models/[email protected]/resnet/v1/weights/m.pt ->
model:///model-15/adeq233td/weights/m.pt
+ * e.g. /dataset/[email protected]/twitter/v1/dir/f.csv ->
dataset:///dataset-15/adeq233td/dir/f.csv
+ * /model/[email protected]/resnet/v1/weights/m.pt ->
model:///model-15/adeq233td/weights/m.pt
*
* @throws java.io.FileNotFoundException if the path is not a valid
versioned-resource path, the
* resource/version does not exist,
or the URI is malformed
@@ -152,10 +152,10 @@ object FileResolver {
)
val (scheme, repositoryName, versionHash) = resourceType match {
- case ResourceType.Datasets =>
+ case ResourceType.Dataset =>
val (repo, hash) = lookupDataset(ownerEmail, resourceName,
versionName, fileName)
(DATASET_FILE_URI_SCHEME, repo, hash)
- case ResourceType.Models =>
+ case ResourceType.Model =>
val (repo, hash) = lookupModel(ownerEmail, resourceName, versionName,
fileName)
(MODEL_FILE_URI_SCHEME, repo, hash)
case other =>
@@ -306,7 +306,7 @@ object FileResolver {
return None
}
parsePrefixedPath(path).collect {
- case (ResourceType.Datasets, ownerEmail, datasetName, _, _) =>
(ownerEmail, datasetName)
+ case (ResourceType.Dataset, ownerEmail, datasetName, _, _) =>
(ownerEmail, datasetName)
}
}
}
diff --git
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/ResourceType.scala
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/ResourceType.scala
index 087dc6ef87..47e90a1818 100644
---
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/ResourceType.scala
+++
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/ResourceType.scala
@@ -26,8 +26,8 @@ package org.apache.texera.amber.core.storage
* Path shape: /<prefix>/ownerEmail/resourceName/versionName/fileRelativePath
*/
object ResourceType extends Enumeration {
- val Datasets: Value = Value("datasets")
- val Models: Value = Value("models")
+ val Dataset: Value = Value("dataset")
+ val Model: Value = Value("model")
/**
* Returns the resource type named by the given path segment, or None if it
is not a known
diff --git
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocument.scala
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocument.scala
index b821ff72f5..55fab33ce2 100644
---
a/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocument.scala
+++
b/common/workflow-core/src/main/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocument.scala
@@ -60,8 +60,8 @@ object LakeFSFileDocument {
*/
def presignEndpointOf(resourceType: ResourceType.Value): String =
resourceType match {
- case ResourceType.Datasets => datasetPresignEndpoint
- case ResourceType.Models => modelPresignEndpoint
+ case ResourceType.Dataset => datasetPresignEndpoint
+ case ResourceType.Model => modelPresignEndpoint
}
}
diff --git
a/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/DocumentFactorySpec.scala
b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/DocumentFactorySpec.scala
index a957819b31..f219383f25 100644
---
a/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/DocumentFactorySpec.scala
+++
b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/DocumentFactorySpec.scala
@@ -137,7 +137,7 @@ class DocumentFactorySpec extends AnyFlatSpec with Matchers
with BeforeAndAfterA
val datasetUri = new URI(s"dataset:///repo/$versionHash/file.txt")
val doc = DocumentFactory.openReadonlyDocument(datasetUri)
doc shouldBe a[LakeFSFileDocument]
- doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Datasets
+ doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Dataset
doc.getURI shouldBe datasetUri
}
@@ -145,7 +145,7 @@ class DocumentFactorySpec extends AnyFlatSpec with Matchers
with BeforeAndAfterA
val modelUri = new URI(s"model:///model-1/$versionHash/weights/model.pt")
val doc = DocumentFactory.openReadonlyDocument(modelUri)
doc shouldBe a[LakeFSFileDocument]
- doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Models
+ doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Model
doc.getURI shouldBe modelUri
val resource = doc.asInstanceOf[OnVersionedFileResource]
@@ -181,7 +181,7 @@ class DocumentFactorySpec extends AnyFlatSpec with Matchers
with BeforeAndAfterA
val datasetUri = new URI(s"dataset:///repo/$versionHash/file.txt")
val (doc, schemaOpt) = DocumentFactory.openDocument(datasetUri)
doc shouldBe a[LakeFSFileDocument]
- doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Datasets
+ doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Dataset
schemaOpt shouldBe None
}
@@ -189,7 +189,7 @@ class DocumentFactorySpec extends AnyFlatSpec with Matchers
with BeforeAndAfterA
val modelUri = new URI(s"model:///model-1/$versionHash/weights/model.pt")
val (doc, schemaOpt) = DocumentFactory.openDocument(modelUri)
doc shouldBe a[LakeFSFileDocument]
- doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Models
+ doc.asInstanceOf[LakeFSFileDocument].resourceType shouldBe
ResourceType.Model
schemaOpt shouldBe None
}
diff --git
a/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocumentSpec.scala
b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocumentSpec.scala
index 56d5cd36ce..fa569f97ca 100644
---
a/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocumentSpec.scala
+++
b/common/workflow-core/src/test/scala/org/apache/texera/amber/core/storage/model/LakeFSFileDocumentSpec.scala
@@ -35,7 +35,7 @@ class LakeFSFileDocumentSpec extends AnyFlatSpec with
Matchers {
private val versionHash = "97fd4c2a755b69b7c66d322eab40b7e5c2ad5d10"
// URI parsing is shared by every resource type; exercise it through the
dataset type.
- private def datasetDoc(uri: URI) = new LakeFSFileDocument(uri,
ResourceType.Datasets)
+ private def datasetDoc(uri: URI) = new LakeFSFileDocument(uri,
ResourceType.Dataset)
"LakeFSFileDocument" should "parse a valid 3-segment dataset URI into its
components" in {
val uri = new URI(s"dataset:///test_dataset/$versionHash/1.txt")
@@ -160,7 +160,7 @@ class LakeFSFileDocumentSpec extends AnyFlatSpec with
Matchers {
"http://localhost:9092/api/dataset/presign-download"
)
.trim
- LakeFSFileDocument.presignEndpointOf(ResourceType.Datasets) shouldBe
expected
+ LakeFSFileDocument.presignEndpointOf(ResourceType.Dataset) shouldBe
expected
}
// Each resource type resolves its own endpoint: a dataset grant must not
authorize a model
@@ -173,9 +173,9 @@ class LakeFSFileDocumentSpec extends AnyFlatSpec with
Matchers {
"http://localhost:9092/api/model/presign-download"
)
.trim
- LakeFSFileDocument.presignEndpointOf(ResourceType.Models) shouldBe expected
- LakeFSFileDocument.presignEndpointOf(ResourceType.Models) should not be
- LakeFSFileDocument.presignEndpointOf(ResourceType.Datasets)
+ LakeFSFileDocument.presignEndpointOf(ResourceType.Model) shouldBe expected
+ LakeFSFileDocument.presignEndpointOf(ResourceType.Model) should not be
+ LakeFSFileDocument.presignEndpointOf(ResourceType.Dataset)
}
// The user JWT token is shared by every LakeFS-backed document, whatever
its resource type.
diff --git
a/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
b/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
index e414371222..2649a3d860 100644
---
a/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
+++
b/common/workflow-core/src/test/scala/org/apache/texera/amber/storage/FileResolverSpec.scala
@@ -122,13 +122,13 @@ class FileResolverSpec
private val localCsvFilePath =
"common/workflow-core/src/test/resources/country_sales_small.csv"
- private val datasetACsvFilePath =
"/datasets/[email protected]/test_dataset/v2/directory/a.csv"
+ private val datasetACsvFilePath =
"/dataset/[email protected]/test_dataset/v2/directory/a.csv"
- private val dataset1TxtFilePath =
"/datasets/[email protected]/test_dataset/v1/1.txt"
+ private val dataset1TxtFilePath =
"/dataset/[email protected]/test_dataset/v1/1.txt"
- private val modelWeightsFilePath =
"/models/[email protected]/test_model/v2/weights/model.pt"
+ private val modelWeightsFilePath =
"/model/[email protected]/test_model/v2/weights/model.pt"
- private val modelReadmeFilePath =
"/models/[email protected]/test_model/v1/README.md"
+ private val modelReadmeFilePath =
"/model/[email protected]/test_model/v1/README.md"
// Legacy unprefixed form — still resolvable as a dataset (backward compat).
private val unprefixedDataset1TxtFilePath =
"/[email protected]/test_dataset/v1/1.txt"
@@ -137,11 +137,11 @@ class FileResolverSpec
private val unknownResourceTypeFilePath =
"/notAResourceType/[email protected]/test_dataset/v1/1.txt"
- // A model name presented under the datasets prefix must not resolve as a
dataset.
- private val modelNameUnderDatasetPrefix =
"/datasets/[email protected]/test_model/v1/README.md"
+ // A model name presented under the dataset prefix must not resolve as a
dataset.
+ private val modelNameUnderDatasetPrefix =
"/dataset/[email protected]/test_model/v1/README.md"
- // A dataset name presented under the models prefix must not resolve as a
model.
- private val datasetNameUnderModelPrefix =
"/models/[email protected]/test_dataset/v1/1.txt"
+ // A dataset name presented under the model prefix must not resolve as a
model.
+ private val datasetNameUnderModelPrefix =
"/model/[email protected]/test_dataset/v1/1.txt"
override protected def beforeAll(): Unit = {
initializeDBAndReplaceDSLContext()
@@ -225,7 +225,7 @@ class FileResolverSpec
"FileResolver" should "throw not found exception when a prefixed path has
too few segments" in {
assertThrows[FileNotFoundException] {
- FileResolver.resolve("/datasets/[email protected]/test_dataset")
+ FileResolver.resolve("/dataset/[email protected]/test_dataset")
}
}
@@ -254,12 +254,12 @@ class FileResolverSpec
"parseDatasetOwnerAndName" should "extract owner email and dataset name from
a valid path" in {
assert(
-
FileResolver.parseDatasetOwnerAndName("/datasets/[email protected]/test_dataset/v1/1.txt")
+
FileResolver.parseDatasetOwnerAndName("/dataset/[email protected]/test_dataset/v1/1.txt")
== Some(("[email protected]", "test_dataset"))
)
// extra segments beyond the file-relative path are ignored
assert(
-
FileResolver.parseDatasetOwnerAndName("/datasets/[email protected]/ds/v2/directory/nested/a.csv")
+
FileResolver.parseDatasetOwnerAndName("/dataset/[email protected]/ds/v2/directory/nested/a.csv")
== Some(("[email protected]", "ds"))
)
}
@@ -272,7 +272,7 @@ class FileResolverSpec
}
it should "return None when the prefixed path has too few segments" in {
-
assert(FileResolver.parseDatasetOwnerAndName("/datasets/[email protected]/ds").isEmpty)
+
assert(FileResolver.parseDatasetOwnerAndName("/dataset/[email protected]/ds").isEmpty)
assert(FileResolver.parseDatasetOwnerAndName("owner/dataset").isEmpty)
}
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala
index fb27257865..26a6496a24 100644
---
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExec.scala
@@ -32,7 +32,7 @@ import org.apache.texera.dao.jooq.generated.tables.User.USER
object FileListerSourceOpExec {
/**
- * Parses a dataset version path
(/datasets/ownerEmail/datasetName/versionName) into its
+ * Parses a dataset version path
(/dataset/ownerEmail/datasetName/versionName) into its
* (resourceTypePrefix, ownerEmail, datasetName, versionName) components.
*
* @throws IllegalArgumentException if the path is not a well-formed
dataset version path
@@ -42,14 +42,14 @@ object FileListerSourceOpExec {
): (String, String, String, String) = {
val segments = datasetVersionPath.split("/").filter(_.nonEmpty)
val invalidPath = s"Invalid dataset version path '$datasetVersionPath'; " +
- "expected /datasets/ownerEmail/datasetName/versionName"
+ "expected /dataset/ownerEmail/datasetName/versionName"
if (segments.headOption.exists(ResourceType.isValidPrefix)) {
require(segments.length >= 4, invalidPath)
(segments(0), segments(1), segments(2), segments(3))
} else {
require(segments.length >= 3, invalidPath)
- (ResourceType.Datasets.toString, segments(0), segments(1), segments(2))
+ (ResourceType.Dataset.toString, segments(0), segments(1), segments(2))
}
}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExecSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExecSpec.scala
index 19fb7513d5..ec0ffd9a29 100644
---
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExecSpec.scala
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/dataset/FileListerSourceOpExecSpec.scala
@@ -23,10 +23,10 @@ import org.scalatest.flatspec.AnyFlatSpec
class FileListerSourceOpExecSpec extends AnyFlatSpec {
- "parseDatasetVersionPath" should "extract components from a
datasets-prefixed path" in {
+ "parseDatasetVersionPath" should "extract components from a dataset-prefixed
path" in {
val (prefix, owner, name, version) =
-
FileListerSourceOpExec.parseDatasetVersionPath("/datasets/[email protected]/twitterDataset/v1")
- assert(prefix == "datasets")
+
FileListerSourceOpExec.parseDatasetVersionPath("/dataset/[email protected]/twitterDataset/v1")
+ assert(prefix == "dataset")
assert(owner == "[email protected]")
assert(name == "twitterDataset")
assert(version == "v1")
@@ -34,7 +34,7 @@ class FileListerSourceOpExecSpec extends AnyFlatSpec {
it should "work when the owner segment is a username without an '@'" in {
val (prefix, owner, name, version) =
-
FileListerSourceOpExec.parseDatasetVersionPath("/datasets/texera/test-ds/v1")
+
FileListerSourceOpExec.parseDatasetVersionPath("/dataset/texera/test-ds/v1")
assert(owner == "texera")
assert(name == "test-ds")
assert(version == "v1")
@@ -42,8 +42,8 @@ class FileListerSourceOpExecSpec extends AnyFlatSpec {
it should "ignore trailing slashes and extra segments" in {
val (prefix, owner, name, version) =
-
FileListerSourceOpExec.parseDatasetVersionPath("/datasets/alice/ds/v2/extra/")
- assert(prefix == "datasets")
+
FileListerSourceOpExec.parseDatasetVersionPath("/dataset/alice/ds/v2/extra/")
+ assert(prefix == "dataset")
assert(owner == "alice")
assert(name == "ds")
assert(version == "v2")
@@ -52,11 +52,11 @@ class FileListerSourceOpExecSpec extends AnyFlatSpec {
"canonicalVersionPath" should "rebuild the prefixed version path from its
components" in {
assert(
FileListerSourceOpExec.canonicalVersionPath(
- "datasets",
+ "dataset",
"[email protected]",
"twitterDataset",
"v1"
- ) == "/datasets/[email protected]/twitterDataset/v1"
+ ) == "/dataset/[email protected]/twitterDataset/v1"
)
}
@@ -64,17 +64,17 @@ class FileListerSourceOpExecSpec extends AnyFlatSpec {
// Emitted paths must be rooted at the parsed components, not the raw
configured path:
// a stray "extra" segment would otherwise leak into every emitted file
path.
val (prefix, owner, name, version) =
-
FileListerSourceOpExec.parseDatasetVersionPath("/datasets/alice/ds/v2/extra/")
+
FileListerSourceOpExec.parseDatasetVersionPath("/dataset/alice/ds/v2/extra/")
assert(
FileListerSourceOpExec.canonicalVersionPath(prefix, owner, name, version)
- == "/datasets/alice/ds/v2"
+ == "/dataset/alice/ds/v2"
)
}
it should "still accept a legacy unprefixed path" in {
val (prefix, owner, name, version) =
FileListerSourceOpExec.parseDatasetVersionPath("/alice/ds/v1")
- assert(prefix == "datasets")
+ assert(prefix == "dataset")
assert(owner == "alice")
assert(name == "ds")
assert(version == "v1")
@@ -85,7 +85,7 @@ class FileListerSourceOpExecSpec extends AnyFlatSpec {
FileListerSourceOpExec.parseDatasetVersionPath("/alice/ds/v1")
assert(
FileListerSourceOpExec.canonicalVersionPath(prefix, owner, name, version)
- == "/datasets/alice/ds/v1"
+ == "/dataset/alice/ds/v1"
)
}
@@ -99,7 +99,7 @@ class FileListerSourceOpExecSpec extends AnyFlatSpec {
it should "reject a prefixed path with too few segments" in {
assertThrows[IllegalArgumentException] {
- FileListerSourceOpExec.parseDatasetVersionPath("/datasets/alice/ds")
+ FileListerSourceOpExec.parseDatasetVersionPath("/dataset/alice/ds")
}
}
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 8bb1a70661..4c8243b850 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
@@ -353,7 +353,7 @@ class DatasetResource extends LazyLogging {
private val COVER_IMAGE_SIZE_LIMIT_BYTES: Long = 10 * 1024 * 1024 // 10 MB
private val ALLOWED_IMAGE_EXTENSIONS: Set[String] = Set(".jpg", ".jpeg",
".png", ".gif", ".webp")
- private val resourceType = ResourceType.Datasets
+ private val resourceType = ResourceType.Dataset
/**
* Helper function to get the dataset from DB with additional information
including user access privilege and owner email
diff --git
a/file-service/src/main/scala/org/apache/texera/service/type/dataset/DatasetFileNode.scala
b/file-service/src/main/scala/org/apache/texera/service/type/dataset/DatasetFileNode.scala
index 821d02045c..85709b8e5b 100644
---
a/file-service/src/main/scala/org/apache/texera/service/type/dataset/DatasetFileNode.scala
+++
b/file-service/src/main/scala/org/apache/texera/service/type/dataset/DatasetFileNode.scala
@@ -25,8 +25,8 @@ import org.apache.texera.amber.core.storage.ResourceType
import scala.collection.mutable
// DatasetFileNode represents a unique file in dataset, its full path is in
the format of:
-// /datasets/ownerEmail/datasetName/versionName/fileRelativePath
-// e.g. /datasets/[email protected]/twitterDataset/v1/california/irvine/tw1.csv
+// /dataset/ownerEmail/datasetName/versionName/fileRelativePath
+// e.g. /dataset/[email protected]/twitterDataset/v1/california/irvine/tw1.csv
class DatasetFileNode(
val name: String, // direct name of this node
val nodeType: String, // "file" or "directory"
@@ -79,10 +79,10 @@ object DatasetFileNode {
): List[DatasetFileNode] = {
val rootNode = new DatasetFileNode("/", "directory", null, "")
- // Root the tree at the datasets prefix node (a directory node named
"datasets").
- val datasetsNode =
- new DatasetFileNode(ResourceType.Datasets.toString, "directory",
rootNode, "")
- rootNode.children = Some(List(datasetsNode))
+ // Root the tree at the dataset prefix node (a directory node named
"dataset").
+ val prefixNode =
+ new DatasetFileNode(ResourceType.Dataset.toString, "directory",
rootNode, "")
+ rootNode.children = Some(List(prefixNode))
// Owner level nodes map
val ownerNodes = mutable.Map[String, DatasetFileNode]()
@@ -91,8 +91,8 @@ object DatasetFileNode {
case ((ownerEmail, datasetName, versionName), objects) =>
val ownerNode = ownerNodes.getOrElseUpdate(
ownerEmail, {
- val newNode = new DatasetFileNode(ownerEmail, "directory",
datasetsNode, ownerEmail)
- datasetsNode.children = Some(datasetsNode.getChildren :+ newNode)
+ val newNode = new DatasetFileNode(ownerEmail, "directory",
prefixNode, ownerEmail)
+ prefixNode.children = Some(prefixNode.getChildren :+ newNode)
newNode
}
)
diff --git
a/file-service/src/test/scala/org/apache/texera/service/type/dataset/DatasetFileNodeSpec.scala
b/file-service/src/test/scala/org/apache/texera/service/type/dataset/DatasetFileNodeSpec.scala
index 160412f8b9..12321d4180 100644
---
a/file-service/src/test/scala/org/apache/texera/service/type/dataset/DatasetFileNodeSpec.scala
+++
b/file-service/src/test/scala/org/apache/texera/service/type/dataset/DatasetFileNodeSpec.scala
@@ -99,13 +99,13 @@ class DatasetFileNodeSpec extends AnyFlatSpec with Matchers
{
Map(("[email protected]", "twitter", "v1") -> objects)
)
- // The tree is rooted at a single "datasets" prefix node; owners nest
under it.
+ // The tree is rooted at a single "dataset" prefix node; owners nest under
it.
roots should have size 1
- val datasetsNode = roots.head
- datasetsNode.getName shouldBe "datasets"
- datasetsNode.getNodeType shouldBe "directory"
+ val prefixNode = roots.head
+ prefixNode.getName shouldBe "dataset"
+ prefixNode.getNodeType shouldBe "directory"
- val ownerNode = datasetsNode.getChildren.find(_.getName ==
"[email protected]").get
+ val ownerNode = prefixNode.getChildren.find(_.getName ==
"[email protected]").get
ownerNode.getNodeType shouldBe "directory"
val datasetNode = ownerNode.getChildren.find(_.getName == "twitter").get
@@ -123,7 +123,7 @@ class DatasetFileNodeSpec extends AnyFlatSpec with Matchers
{
val file1 = bDir.getChildren.find(_.getName == "1.csv").get
file1.getNodeType shouldBe "file"
file1.getSize shouldBe Some(2L)
- file1.getFilePath shouldBe "/datasets/[email protected]/twitter/v1/b/1.csv"
+ file1.getFilePath shouldBe "/dataset/[email protected]/twitter/v1/b/1.csv"
// Total size equals the sum of the three files.
DatasetFileNode.calculateTotalSize(roots) shouldBe 6L
diff --git a/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
b/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
index 64231dd156..207182e5fd 100644
--- a/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
+++ b/frontend/src/app/common/type/datasetVersionFileTree.spec.ts
@@ -41,7 +41,7 @@ describe("getRelativePathFromDatasetFileNode", () => {
const node: DatasetFileNode = {
name: "tw1.csv",
type: "file",
- parentDir:
"/datasets/[email protected]/twitterDataset/v1/california/irvine",
+ parentDir:
"/dataset/[email protected]/twitterDataset/v1/california/irvine",
};
expect(getRelativePathFromDatasetFileNode(node)).toBe("california/irvine/tw1.csv");
});
@@ -50,7 +50,7 @@ describe("getRelativePathFromDatasetFileNode", () => {
const node: DatasetFileNode = {
name: "readme.txt",
type: "file",
- parentDir: "/datasets/[email protected]/twitterDataset/v1",
+ parentDir: "/dataset/[email protected]/twitterDataset/v1",
};
expect(getRelativePathFromDatasetFileNode(node)).toBe("readme.txt");
});
@@ -59,7 +59,7 @@ describe("getRelativePathFromDatasetFileNode", () => {
const node: DatasetFileNode = {
name: "v1",
type: "directory",
- parentDir: "/datasets/[email protected]/twitterDataset",
+ parentDir: "/dataset/[email protected]/twitterDataset",
};
expect(getRelativePathFromDatasetFileNode(node)).toBe("");
});
@@ -68,7 +68,7 @@ describe("getRelativePathFromDatasetFileNode", () => {
const node: DatasetFileNode = {
name: "f.csv",
type: "file",
- parentDir: "/datasets/[email protected]/twitterDataset//v1/sub",
+ parentDir: "/dataset/[email protected]/twitterDataset//v1/sub",
};
expect(getRelativePathFromDatasetFileNode(node)).toBe("sub/f.csv");
});
diff --git a/frontend/src/app/common/type/datasetVersionFileTree.ts
b/frontend/src/app/common/type/datasetVersionFileTree.ts
index 98f898d443..efbb875128 100644
--- a/frontend/src/app/common/type/datasetVersionFileTree.ts
+++ b/frontend/src/app/common/type/datasetVersionFileTree.ts
@@ -32,7 +32,7 @@ export function getFullPathFromDatasetFileNode(node:
DatasetFileNode): string {
/**
* Returns the relative path of a DatasetFileNode by stripping the first four
segments
- * (datasets/ownerEmail/datasetName/versionName).
+ * (dataset/ownerEmail/datasetName/versionName).
* @param node The DatasetFileNode whose relative path is needed.
* @returns The relative path (without the first four segments and without a
leading slash).
*/
diff --git a/frontend/src/app/common/type/resource-type.ts
b/frontend/src/app/common/type/resource-type.ts
index a129c917e2..22c4a8592a 100644
--- a/frontend/src/app/common/type/resource-type.ts
+++ b/frontend/src/app/common/type/resource-type.ts
@@ -17,8 +17,8 @@
*/
/**
- * The leading segment of a logical file path, identifying the resource kind
(e.g. /datasets/...).
+ * The leading segment of a logical file path, identifying the resource kind
(e.g. /dataset/...).
*/
export enum ResourceType {
- Datasets = "datasets",
+ Dataset = "dataset",
}
diff --git
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
index 78c35761a4..7bb6af6e0d 100644
---
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
+++
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.spec.ts
@@ -2694,7 +2694,7 @@ describe("DatasetDetailComponent rendered template", ()
=> {
const leaf = (name: string): DatasetFileNode => ({
name,
type: "file",
- parentDir: `/datasets/${OWNER}/ds/v1/nested`,
+ parentDir: `/dataset/${OWNER}/ds/v1/nested`,
size: 2048,
});
@@ -2719,7 +2719,7 @@ describe("DatasetDetailComponent rendered template", ()
=> {
// The heading is the full path — the copy-path button beside it copies
// exactly this string — not the bare file name or the relative path.
expect(text(q<HTMLElement>(fixture.nativeElement,
".file-title-main"))).toBe(
- `/datasets/${OWNER}/ds/v1/nested/b.csv`
+ `/dataset/${OWNER}/ds/v1/nested/b.csv`
);
// 2048 bytes reaches the reader as a human-readable size, not as a raw
count.
expect(text(q<HTMLElement>(fixture.nativeElement,
".file-size"))).toBe("2.00 KB");
diff --git
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.spec.ts
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.spec.ts
index 806d2a6f25..ff9e82609a 100644
---
a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.spec.ts
+++
b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-version-filetree/user-dataset-version-filetree.component.spec.ts
@@ -183,7 +183,7 @@ describe("UserDatasetVersionFiletreeComponent", () => {
const file = (name: string): DatasetFileNode => ({
name,
type: "file",
- parentDir: "/datasets/owner/dataset/v1",
+ parentDir: "/dataset/owner/dataset/v1",
});
// Deliberately synchronous: the tree keeps a pending timer, so awaiting
whenStable()
@@ -262,9 +262,9 @@ describe("UserDatasetVersionFiletreeComponent", () => {
const emitted: string[] = [];
component.setCoverImage.subscribe((path: string) => emitted.push(path));
- // parentDir has exactly the four stripped segments
(datasets/owner/dataset/version),
+ // parentDir has exactly the four stripped segments
(dataset/owner/dataset/version),
// so the relative path is just the file name.
- component.onSetCover({ name: "photo.png", type: "file", parentDir:
"/datasets/owner/dataset/v1" });
+ component.onSetCover({ name: "photo.png", type: "file", parentDir:
"/dataset/owner/dataset/v1" });
expect(emitted).toEqual(["photo.png"]);
});
diff --git
a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.spec.ts
b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.spec.ts
index 604d86a9f9..f42ae00a81 100644
---
a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.spec.ts
+++
b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.spec.ts
@@ -111,7 +111,7 @@ describe("DatasetSelectionModalComponent", () => {
it("ngOnInit initializes selectedDataset and selectedVersion from
data.selectedPath", () => {
modalData.fileMode = true;
- modalData.selectedPath = `/datasets/${OWNER}/myds/v1`;
+ modalData.selectedPath = `/dataset/${OWNER}/myds/v1`;
build();
@@ -154,7 +154,7 @@ describe("DatasetSelectionModalComponent", () => {
expect(datasetService.retrieveDatasetVersionFileTree).toHaveBeenCalledWith(10,
100);
expect(component.fileTree).toEqual([fileNode]);
- expect(component.selectedPath).toBe(`/datasets/${OWNER}/myds/v1`);
+ expect(component.selectedPath).toBe(`/dataset/${OWNER}/myds/v1`);
});
it("onFileSelected sets selectedPath to the node's full path in file mode",
() => {
@@ -303,12 +303,12 @@ describe("DatasetSelectionModalComponent", () => {
confirmButton().click();
-
expect(modalRef.close).toHaveBeenCalledWith(`/datasets/${OWNER}/myds/v1`);
+ expect(modalRef.close).toHaveBeenCalledWith(`/dataset/${OWNER}/myds/v1`);
});
it("takes the path from the file tree's selection in file mode", () => {
modalData.fileMode = true;
- modalData.selectedPath = `/datasets/${OWNER}/myds/v1`;
+ modalData.selectedPath = `/dataset/${OWNER}/myds/v1`;
build();
fixture.debugElement
diff --git
a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts
b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts
index 97ecf579ca..b5768aa685 100644
---
a/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts
+++
b/frontend/src/app/workspace/component/dataset-selection-modal/dataset-selection-modal.component.ts
@@ -85,7 +85,7 @@ export class DatasetSelectionModalComponent implements OnInit
{
const selectedPath = this.data.selectedPath;
if (selectedPath) {
const segments = selectedPath.split("/").filter(part => part.length
> 0);
- // TODO(datasets-prefix): require the prefix once all ml model
support PRs are done.
+ // TODO(dataset-prefix): require the prefix once all ml model
support PRs are done.
if ((Object.values(ResourceType) as string[]).includes(segments[0]))
{
segments.shift();
}
@@ -124,7 +124,7 @@ export class DatasetSelectionModalComponent implements
OnInit {
this.fileTree = data.fileNodes;
});
if (!this.data.fileMode) {
- this.selectedPath =
`/${ResourceType.Datasets}/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}`;
+ this.selectedPath =
`/${ResourceType.Dataset}/${this.selectedDataset.ownerEmail}/${this.selectedDataset.dataset.name}/${this.selectedVersion.name}`;
}
}
}
diff --git
a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.spec.ts
b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.spec.ts
index 394e9773cc..f5ba4045e8 100644
---
a/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.spec.ts
+++
b/frontend/src/app/workspace/component/dataset-version-selector/dataset-version-selector.component.spec.ts
@@ -101,11 +101,11 @@ describe("DatasetVersionSelectorComponent", () => {
});
it("shows the chosen path in a read-only box", () => {
- setFormControl("/datasets/[email protected]/myds/v1");
+ setFormControl("/dataset/[email protected]/myds/v1");
fixture.detectChanges();
const box = selectionBox()!;
- expect(box.value).toBe("/datasets/[email protected]/myds/v1");
+ expect(box.value).toBe("/dataset/[email protected]/myds/v1");
// The path is only editable through the picker, never by typing.
expect(box.readOnly).toBe(true);
expect(box.required).toBe(true);
@@ -114,13 +114,13 @@ describe("DatasetVersionSelectorComponent", () => {
it("opens the picker from the button and shows whatever it returns", () =>
{
setFormControl("");
fixture.detectChanges();
- modalServiceSpy.create.mockReturnValue({ afterClose:
of("/datasets/[email protected]/myds/v2") });
+ modalServiceSpy.create.mockReturnValue({ afterClose:
of("/dataset/[email protected]/myds/v2") });
openButton().click();
fixture.detectChanges();
expect(modalServiceSpy.create).toHaveBeenCalledTimes(1);
- expect(selectionBox()!.value).toBe("/datasets/[email protected]/myds/v2");
+ expect(selectionBox()!.value).toBe("/dataset/[email protected]/myds/v2");
});
});
});
diff --git a/sql/changelog.xml b/sql/changelog.xml
index c0f735a09c..b9c96431a5 100644
--- a/sql/changelog.xml
+++ b/sql/changelog.xml
@@ -104,6 +104,11 @@
<sqlFile path="sql/updates/38.sql"/>
</changeSet>
+ <!-- Singularize the resource-type prefix on stored dataset paths -->
+ <changeSet id="39" author="tanishqgandhi1908">
+ <sqlFile path="sql/updates/39.sql"/>
+ </changeSet>
+
<!-- example changeSet
<changeSet id="1" author="author">
<sqlFile path="sql/updates/1.sql"/>
diff --git a/sql/updates/39.sql b/sql/updates/39.sql
new file mode 100644
index 0000000000..4c48244bdd
--- /dev/null
+++ b/sql/updates/39.sql
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+
+\c texera_db
+
+SET search_path TO texera_db;
+
+BEGIN;
+
+-- The resource-type prefix on dataset logical paths is singular, matching the
table
+-- it names: /dataset/ownerEmail/datasetName/versionName/... This supersedes
36.sql,
+-- which introduced the prefix in its plural form; that changeSet is left as it
+-- shipped, so databases that already recorded it are brought forward here
instead.
+--
+-- Stored workflows carry such paths inside workflow.content and
+-- workflow_version.content, in two operator properties:
+-- * fileName (scan-source operators): /owner/name/version/file
+-- * datasetVersionPath (file-lister operator): /owner/name/version
+--
+-- Both are normalized to that form: a "/datasets/" prefix has its leading
segment
+-- rewritten, and a path with no prefix gets "dataset" prepended. The prefixed
case
+-- is tested first, since such a path looks unprefixed to the other branch and
must
+-- not be prefixed twice.
+--
+-- Either case applies only when the path's owner and name segments match an
+-- existing (user.email, dataset.name) pair -- unique, and read at parts 1 and
2
+-- unprefixed, 2 and 3 prefixed. That guard is what keeps the migration off the
+-- plain filesystem paths and URLs this column also holds: "/datasets" is an
+-- ordinary directory name, and FileResolver tries localResolveFunc first, so a
+-- fileName of /datasets/imdb/movies.csv can be a working local mount. It
matches
+-- no dataset, so it is left alone.
+--
+-- Values already in the target form match neither case, making this
idempotent.
+-- jsonb_set uses create_missing = false so absent properties are never added.
+
+DO $$
+DECLARE
+ wf_count INT := 0;
+ wv_count INT := 0;
+BEGIN
+ WITH affected AS (
+ SELECT w.wid
+ FROM workflow w,
+ jsonb_array_elements(
+ CASE
+ WHEN jsonb_typeof(w.content::jsonb -> 'operators') =
'array'
+ THEN w.content::jsonb -> 'operators'
+ ELSE '[]'::jsonb
+ END
+ ) AS op,
+ LATERAL (
+ SELECT op #>> '{operatorProperties,fileName}' AS fn,
+ op #>> '{operatorProperties,datasetVersionPath}' AS dvp
+ ) f
+ WHERE jsonb_typeof(w.content::jsonb -> 'operators') = 'array'
+ AND (
+ (f.fn ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.fn, '/'), '/',
2)
+ AND d.name = split_part(ltrim(f.fn, '/'), '/',
3)))
+ OR
+ (f.dvp ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp, '/'),
'/', 2)
+ AND d.name = split_part(ltrim(f.dvp, '/'),
'/', 3)))
+ OR
+ (f.fn IS NOT NULL AND left(f.fn, 9) <> '/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.fn, '/'), '/',
1)
+ AND d.name = split_part(ltrim(f.fn, '/'), '/',
2)))
+ OR
+ (f.dvp IS NOT NULL AND left(f.dvp, 9) <> '/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp, '/'),
'/', 1)
+ AND d.name = split_part(ltrim(f.dvp, '/'),
'/', 2)))
+ )
+ GROUP BY w.wid
+ ),
+ updated AS (
+ UPDATE workflow w
+ SET content = jsonb_set(w.content::jsonb, '{operators}', (
+ SELECT jsonb_agg(
+ jsonb_set(
+ jsonb_set(
+ op,
+ '{operatorProperties,fileName}',
+ CASE
+ WHEN f.fn ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u
ON d.owner_uid = u.uid
+ WHERE u.email =
split_part(ltrim(f.fn, '/'), '/', 2)
+ AND d.name =
split_part(ltrim(f.fn, '/'), '/', 3))
+ THEN to_jsonb(regexp_replace(f.fn, '^/datasets/',
'/dataset/'))
+ WHEN f.fn IS NOT NULL AND left(f.fn, 9) <>
'/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u
ON d.owner_uid = u.uid
+ WHERE u.email =
split_part(ltrim(f.fn, '/'), '/', 1)
+ AND d.name =
split_part(ltrim(f.fn, '/'), '/', 2))
+ THEN to_jsonb('/dataset/' || ltrim(f.fn, '/'))
+ ELSE COALESCE(op #>
'{operatorProperties,fileName}', 'null'::jsonb)
+ END,
+ false
+ ),
+ '{operatorProperties,datasetVersionPath}',
+ CASE
+ WHEN f.dvp ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp,
'/'), '/', 2)
+ AND d.name = split_part(ltrim(f.dvp,
'/'), '/', 3))
+ THEN to_jsonb(regexp_replace(f.dvp, '^/datasets/',
'/dataset/'))
+ WHEN f.dvp IS NOT NULL AND left(f.dvp, 9) <>
'/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp,
'/'), '/', 1)
+ AND d.name = split_part(ltrim(f.dvp,
'/'), '/', 2))
+ THEN to_jsonb('/dataset/' || ltrim(f.dvp, '/'))
+ ELSE COALESCE(op #>
'{operatorProperties,datasetVersionPath}', 'null'::jsonb)
+ END,
+ false
+ )
+ ORDER BY ord
+ )
+ FROM jsonb_array_elements(
+ CASE
+ WHEN jsonb_typeof(w.content::jsonb -> 'operators') =
'array'
+ THEN w.content::jsonb -> 'operators'
+ ELSE '[]'::jsonb
+ END
+ ) WITH ORDINALITY AS t(op, ord),
+ LATERAL (
+ SELECT op #>> '{operatorProperties,fileName}'
AS fn,
+ op #>> '{operatorProperties,datasetVersionPath}'
AS dvp
+ ) f
+ ))::text
+ FROM affected a
+ WHERE w.wid = a.wid
+ RETURNING 1
+ )
+ SELECT count(*) INTO wf_count FROM updated;
+
+ WITH affected AS (
+ SELECT wv.vid
+ FROM workflow_version wv,
+ jsonb_array_elements(
+ CASE
+ WHEN jsonb_typeof(wv.content::jsonb -> 'operators') =
'array'
+ THEN wv.content::jsonb -> 'operators'
+ ELSE '[]'::jsonb
+ END
+ ) AS op,
+ LATERAL (
+ SELECT op #>> '{operatorProperties,fileName}' AS fn,
+ op #>> '{operatorProperties,datasetVersionPath}' AS dvp
+ ) f
+ WHERE jsonb_typeof(wv.content::jsonb -> 'operators') = 'array'
+ AND (
+ (f.fn ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.fn, '/'), '/',
2)
+ AND d.name = split_part(ltrim(f.fn, '/'), '/',
3)))
+ OR
+ (f.dvp ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp, '/'),
'/', 2)
+ AND d.name = split_part(ltrim(f.dvp, '/'),
'/', 3)))
+ OR
+ (f.fn IS NOT NULL AND left(f.fn, 9) <> '/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.fn, '/'), '/',
1)
+ AND d.name = split_part(ltrim(f.fn, '/'), '/',
2)))
+ OR
+ (f.dvp IS NOT NULL AND left(f.dvp, 9) <> '/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp, '/'),
'/', 1)
+ AND d.name = split_part(ltrim(f.dvp, '/'),
'/', 2)))
+ )
+ GROUP BY wv.vid
+ ),
+ updated AS (
+ UPDATE workflow_version wv
+ SET content = jsonb_set(wv.content::jsonb, '{operators}', (
+ SELECT jsonb_agg(
+ jsonb_set(
+ jsonb_set(
+ op,
+ '{operatorProperties,fileName}',
+ CASE
+ WHEN f.fn ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u
ON d.owner_uid = u.uid
+ WHERE u.email =
split_part(ltrim(f.fn, '/'), '/', 2)
+ AND d.name =
split_part(ltrim(f.fn, '/'), '/', 3))
+ THEN to_jsonb(regexp_replace(f.fn, '^/datasets/',
'/dataset/'))
+ WHEN f.fn IS NOT NULL AND left(f.fn, 9) <>
'/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u
ON d.owner_uid = u.uid
+ WHERE u.email =
split_part(ltrim(f.fn, '/'), '/', 1)
+ AND d.name =
split_part(ltrim(f.fn, '/'), '/', 2))
+ THEN to_jsonb('/dataset/' || ltrim(f.fn, '/'))
+ ELSE COALESCE(op #>
'{operatorProperties,fileName}', 'null'::jsonb)
+ END,
+ false
+ ),
+ '{operatorProperties,datasetVersionPath}',
+ CASE
+ WHEN f.dvp ~ '^/datasets/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp,
'/'), '/', 2)
+ AND d.name = split_part(ltrim(f.dvp,
'/'), '/', 3))
+ THEN to_jsonb(regexp_replace(f.dvp, '^/datasets/',
'/dataset/'))
+ WHEN f.dvp IS NOT NULL AND left(f.dvp, 9) <>
'/dataset/'
+ AND EXISTS (SELECT 1 FROM dataset d JOIN "user" u ON
d.owner_uid = u.uid
+ WHERE u.email = split_part(ltrim(f.dvp,
'/'), '/', 1)
+ AND d.name = split_part(ltrim(f.dvp,
'/'), '/', 2))
+ THEN to_jsonb('/dataset/' || ltrim(f.dvp, '/'))
+ ELSE COALESCE(op #>
'{operatorProperties,datasetVersionPath}', 'null'::jsonb)
+ END,
+ false
+ )
+ ORDER BY ord
+ )
+ FROM jsonb_array_elements(
+ CASE
+ WHEN jsonb_typeof(wv.content::jsonb -> 'operators') =
'array'
+ THEN wv.content::jsonb -> 'operators'
+ ELSE '[]'::jsonb
+ END
+ ) WITH ORDINALITY AS t(op, ord),
+ LATERAL (
+ SELECT op #>> '{operatorProperties,fileName}'
AS fn,
+ op #>> '{operatorProperties,datasetVersionPath}'
AS dvp
+ ) f
+ ))::text
+ FROM affected a
+ WHERE wv.vid = a.vid
+ RETURNING 1
+ )
+ SELECT count(*) INTO wv_count FROM updated;
+
+ RAISE NOTICE 'Normalized the resource-type path prefix in % workflow and %
workflow_version row(s).', wf_count, wv_count;
+END $$;
+
+COMMIT;