Skip to content

Allow reuse of project name and version when different parent project#1123

Draft
jhoward-lm wants to merge 7 commits intoDependencyTrack:mainfrom
jhoward-lm:unique-name-version-within-parent-project
Draft

Allow reuse of project name and version when different parent project#1123
jhoward-lm wants to merge 7 commits intoDependencyTrack:mainfrom
jhoward-lm:unique-name-version-within-parent-project

Conversation

@jhoward-lm
Copy link
Copy Markdown
Contributor

Description

This PR modifies the existing unique constraint on a project's combined name and version columns to include the parent project ID as an additional factor defining uniqueness.

The goal is to allow reuse of a project name, or project name and version, when the child projects have different parent projects.

---
title: New Behavior
---
erDiagram
    p1["parent project"] {
        NAME parent-project
        VERSION v1
    }

    p2["parent project"] {
        NAME parent-project
        VERSION v2
    }

    c1["child project"] {
        NAME child-project
        VERSION v1
    }

    c2["child project"] {
        NAME child-project
        VERSION v2
    }

    c3["child project"] {
        NAME child-project
        VERSION v1
    }

    c4["child project"] {
        NAME child-project
        VERSION v2
    }

    p1 |o--o{ c1 : "parent of"
    p1 |o--o{ c2 : "parent of"
    p2 |o--o{ c3 : "parent of"
    p2 |o--o{ c4 : "parent of"
Loading

Addressed Issue

Additional Details

Checklist

  • I have read and understand the contributing guidelines
  • This PR fixes a defect, and I have provided tests to verify that the fix is effective
  • This PR implements an enhancement, and I have provided tests to verify that it works as intended
  • This PR introduces changes to the database model, and I have updated the migration changelog accordingly
  • This PR introduces new or alters existing behavior, and I have updated the documentation accordingly

@nscuro nscuro added the enhancement New feature or request label Apr 9, 2025
@nscuro nscuro added this to the 5.6.0 milestone Apr 9, 2025
@jhoward-lm
Copy link
Copy Markdown
Contributor Author

@nscuro Do you think there are any changes to the models that will be needed to support this?

@nscuro
Copy link
Copy Markdown
Member

nscuro commented Apr 9, 2025

createProjectWithoutVersionDuplicateTest is failing because we don't detect duplicates when both VERSION and PARENT_PROJECT_ID are null.

I think an additional index is needed:

CREATE UNIQUE INDEX "PROJECT_NAME_NULL_PARENT_NULL_VERSION_IDX"
    ON "PROJECT" ("NAME")
 WHERE "VERSION" IS NULL
   AND "PARENT_PROJECT_ID" IS NULL;

@nscuro
Copy link
Copy Markdown
Member

nscuro commented Apr 9, 2025

@jhoward-lm Do you think there are any changes to the models that will be needed to support this?

Upon initial look, there is this method which checks whether a given name-version combination already exists:

/**
* Check whether a {@link Project} with a given {@code name} and {@code version} exists.
*
* @param name Name of the {@link Project} to check for
* @param version Version of the {@link Project} to check for
* @return {@code true} when a matching {@link Project} exists, otherwise {@code false}
* @since 4.9.0
*/
@Override
public boolean doesProjectExist(final String name, final String version) {
final Query<Project> query = pm.newQuery(Project.class);
if (version != null) {
query.setFilter("name == :name && version == :version");
query.setNamedParameters(Map.of(
"name", name,
"version", version
));
} else {
// Version is optional for projects, but using null
// for parameter values bypasses the query compilation cache.
// https://github.com/DependencyTrack/dependency-track/issues/2540
query.setFilter("name == :name && version == null");
query.setNamedParameters(Map.of(
"name", name
));
}
query.setResult("count(this)");
try {
return query.executeResultUnique(Long.class) > 0;
} finally {
query.closeAll();
}
}

This would need updating to take the parent project into consideration.

We also have this lookup method for projects which, given a name and version, can only return a single result:

@GET
@Path("/lookup")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Returns a specific project by its name and version",
operationId = "getProjectByNameAndVersion",
description = "<p>Requires permission <strong>VIEW_PORTFOLIO</strong></p>"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "A specific project by its name and version",
content = @Content(schema = @Schema(implementation = Project.class))
),
@ApiResponse(responseCode = "401", description = "Unauthorized"),
@ApiResponse(
responseCode = "403",
description = "Access to the requested project is forbidden",
content = @Content(schema = @Schema(implementation = ProblemDetails.class), mediaType = ProblemDetails.MEDIA_TYPE_JSON)),
@ApiResponse(responseCode = "404", description = "The project could not be found")
})
@PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO)
public Response getProject(
@Parameter(description = "The name of the project to query on", required = true)
@QueryParam("name") String name,
@Parameter(description = "The version of the project to query on", required = true)
@QueryParam("version") String version) {
try (QueryManager qm = new QueryManager()) {
final Project project = qm.getProject(name, version);
if (project != null) {
requireAccess(qm, project);
return Response.ok(project).build();
} else {
return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build();
}
}
}

The logic to retrieve all versions of a project is also solely based on the project name at the moment:

private List<ProjectVersion> getProjectVersions(Project project) {
final Query<Project> query = pm.newQuery(Project.class);
query.setFilter("name == :name");
query.setParameters(project.getName());
query.setResult("uuid, version, inactiveSince");
query.setOrdering("id asc"); // Ensure consistent ordering
return query.executeResultList(ProjectVersion.class);

There are multiple usages of this method, which retrieves a single project by name and version:

/**
* Returns a project by its name and version.
*
* @param name the name of the Project (required)
* @param version the version of the Project (or null)
* @return a Project object, or null if not found
*/
@Override
public Project getProject(final String name, final String version) {
final Query<Project> query = pm.newQuery(Project.class);
final var filterBuilder = new ProjectQueryFilterBuilder()
.withName(name)
.withVersion(version);
final String queryFilter = filterBuilder.buildFilter();
final Map<String, Object> params = filterBuilder.getParams();
preprocessACLs(query, queryFilter, params, false);
query.setFilter(queryFilter);
query.setRange(0, 1);
final Project project = singleResult(query.executeWithMap(params));
if (project != null) {
// set Metrics to prevent extra round trip
project.setMetrics(getMostRecentProjectMetrics(project));
// set ProjectVersions to prevent extra round trip
project.setVersions(getProjectVersions(project));
}
return project;
}

@jhoward-lm
Copy link
Copy Markdown
Contributor Author

createProjectWithoutVersionDuplicateTest is failing because we don't detect duplicates when both VERSION and PARENT_PROJECT_ID are null.

I think an additional index is needed:

CREATE UNIQUE INDEX "PROJECT_NAME_NULL_PARENT_NULL_VERSION_IDX"
    ON "PROJECT" ("NAME")
 WHERE "VERSION" IS NULL
   AND "PARENT_PROJECT_ID" IS NULL;

You're right, I had already created it and testing locally now.

Also, do the existing non-unique indexes PROJECT_NAME_IDX and PROJECT_VERSION_IDX need to stay, or is their functionality covered by the new unique indexes on those columns?

@nscuro
Copy link
Copy Markdown
Member

nscuro commented Apr 9, 2025

Also, do the existing non-unique indexes PROJECT_NAME_IDX and PROJECT_VERSION_IDX need to stay, or is their functionality covered by the new unique indexes on those columns?

They'd be covered with these new indexes so are safe to drop.

@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Apr 9, 2025

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
+0.01% (target: -1.00%) 97.06% (target: 70.00%)
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (c1f0123) 22861 19238 84.15%
Head commit (a1f8e27) 22874 (+13) 19251 (+13) 84.16% (+0.01%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#1123) 34 33 97.06%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@jhoward-lm
Copy link
Copy Markdown
Contributor Author

Going to defer this one until after resolution of #1121 and DependencyTrack/hyades#1736/DependencyTrack/hyades#1759

@jhoward-lm jhoward-lm marked this pull request as draft April 15, 2025 14:13
jhoward-lm and others added 7 commits May 10, 2025 17:22
Signed-off-by: Jonathan Howard <jonathan.w.howard@lmco.com>
Co-authored-by: Niklas <nscuro@protonmail.com>
Signed-off-by: jhoward-lm <140011346+jhoward-lm@users.noreply.github.com>
Signed-off-by: Jonathan Howard <jonathan.w.howard@lmco.com>
Signed-off-by: Jonathan Howard <jonathan.w.howard@lmco.com>
- add optional "parent" query param to ProjectResource.getProject
- revert any non-required changes

Signed-off-by: Jonathan Howard <jonathan.w.howard@lmco.com>
Signed-off-by: Jonathan Howard <jonathan.w.howard@lmco.com>
Signed-off-by: Jonathan Howard <jonathan.w.howard@lmco.com>
@jhoward-lm jhoward-lm force-pushed the unique-name-version-within-parent-project branch from f2ac75e to 55cce1a Compare May 10, 2025 22:37
@nscuro nscuro removed this from the 5.6.0 milestone Jul 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants