Skip to content

Commit b7e4bca

Browse files
committed
Materialize project hierarchies in separate table
Signed-off-by: nscuro <nscuro@protonmail.com>
1 parent 85f87a0 commit b7e4bca

File tree

7 files changed

+159
-200
lines changed

7 files changed

+159
-200
lines changed

src/main/java/org/dependencytrack/persistence/PolicyQueryManager.java

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -95,71 +95,6 @@ public List<Policy> getAllPolicies() {
9595
return query.executeList();
9696
}
9797

98-
/**
99-
* Fetch all {@link Policy}s that are applicable to a given {@link Project}.
100-
*
101-
* @param project The {@link Project} to fetch {@link Policy}s for
102-
* @return A {@link List} of {@link Policy}s.
103-
* @since 5.0.0
104-
*/
105-
public List<Policy> getApplicablePolicies(final Project project) {
106-
var filter = """
107-
(this.projects.isEmpty() && this.tags.isEmpty())
108-
|| (this.projects.contains(:project)
109-
""";
110-
var params = new HashMap<String, Object>();
111-
params.put("project", project);
112-
113-
// To compensate for missing support for recursion of Common Table Expressions (CTEs)
114-
// in JDO, we have to fetch the UUIDs of all parent projects upfront. Otherwise, we'll
115-
// not be able to evaluate whether the policy is inherited from parent projects.
116-
var variables = "";
117-
final List<UUID> parentUuids = getParents(project);
118-
if (!parentUuids.isEmpty()) {
119-
filter += """
120-
|| (this.includeChildren
121-
&& this.projects.contains(parentVar)
122-
&& :parentUuids.contains(parentVar.uuid))
123-
""";
124-
variables += "org.dependencytrack.model.Project parentVar";
125-
params.put("parentUuids", parentUuids);
126-
}
127-
filter += ")";
128-
129-
// DataNucleus generates an invalid SQL query when using the idiomatic solution.
130-
// The following works, but it's ugly and likely doesn't perform well if the project
131-
// has many tags. Worth trying the idiomatic way again once DN has been updated to > 6.0.4.
132-
//
133-
// filter += "m|| (this.tags.contains(commonTag) && :project.tags.contains(commonTag))";
134-
// variables += "org.dependencytrack.model.Tag commonTag";
135-
if (project.getTags() != null && !project.getTags().isEmpty()) {
136-
filter += " || (";
137-
for (int i = 0; i < project.getTags().size(); i++) {
138-
filter += "this.tags.contains(:tag" + i + ")";
139-
params.put("tag" + i, project.getTags().get(i));
140-
if (i < (project.getTags().size() - 1)) {
141-
filter += " || ";
142-
}
143-
}
144-
filter += ")";
145-
}
146-
147-
final List<Policy> policies;
148-
final Query<Policy> query = pm.newQuery(Policy.class);
149-
try {
150-
query.setFilter(filter);
151-
query.setNamedParameters(params);
152-
if (!variables.isEmpty()) {
153-
query.declareVariables(variables);
154-
}
155-
policies = List.copyOf(query.executeList());
156-
} finally {
157-
query.closeAll();
158-
}
159-
160-
return policies;
161-
}
162-
16398
/**
16499
* Returns a policy by it's name.
165100
* @param name the name of the policy (required)

src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,37 +1242,6 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final
12421242
return result;
12431243
}
12441244

1245-
/**
1246-
* Fetch the {@link UUID}s of all parents of a given {@link Project}.
1247-
*
1248-
* @param project The {@link Project} to fetch the parent {@link UUID}s for
1249-
* @return A {@link List} of {@link UUID}s
1250-
*/
1251-
@Override
1252-
public List<UUID> getParents(final Project project) {
1253-
return getParents(project.getUuid(), new ArrayList<>());
1254-
}
1255-
1256-
private List<UUID> getParents(final UUID uuid, final List<UUID> parents) {
1257-
final UUID parentUuid;
1258-
final Query<Project> query = pm.newQuery(Project.class);
1259-
try {
1260-
query.setFilter("uuid == :uuid && parent != null");
1261-
query.setParameters(uuid);
1262-
query.setResult("parent.uuid");
1263-
parentUuid = query.executeResultUnique(UUID.class);
1264-
} finally {
1265-
query.closeAll();
1266-
}
1267-
1268-
if (parentUuid == null) {
1269-
return parents;
1270-
}
1271-
1272-
parents.add(parentUuid);
1273-
return getParents(parentUuid, parents);
1274-
}
1275-
12761245
/**
12771246
* Check whether a {@link Project} with a given {@code name} and {@code version} exists.
12781247
*
@@ -1307,30 +1276,43 @@ public boolean doesProjectExist(final String name, final String version) {
13071276
}
13081277
}
13091278

1310-
private static boolean isChildOf(Project project, UUID uuid) {
1311-
boolean isChild = false;
1312-
if (project.getParent() != null) {
1313-
if (project.getParent().getUuid().equals(uuid)) {
1314-
return true;
1315-
} else {
1316-
isChild = isChildOf(project.getParent(), uuid);
1317-
}
1279+
private boolean isChildOf(Project project, UUID uuid) {
1280+
final Query<?> query = pm.newQuery(Query.SQL, /* language=SQL */ """
1281+
SELECT EXISTS(
1282+
SELECT 1
1283+
FROM "PROJECT_HIERARCHY"
1284+
WHERE "PARENT_PROJECT_ID" = (SELECT "ID" FROM "PROJECT" WHERE "UUID" = ?)
1285+
AND "CHILD_PROJECT_ID" = ?
1286+
)
1287+
""");
1288+
query.setParameters(uuid, project.getId());
1289+
try {
1290+
return (boolean) query.executeUnique();
1291+
} finally {
1292+
query.closeAll();
13181293
}
1319-
return isChild;
13201294
}
13211295

1322-
private static boolean hasActiveChild(Project project) {
1323-
boolean hasActiveChild = false;
1324-
if (project.getChildren() != null) {
1325-
for (Project child : project.getChildren()) {
1326-
if (child.isActive() || hasActiveChild) {
1327-
return true;
1328-
} else {
1329-
hasActiveChild = hasActiveChild(child);
1330-
}
1331-
}
1296+
private boolean hasActiveChild(Project project) {
1297+
final Query<?> query = pm.newQuery(Query.SQL, /* language=SQL */ """
1298+
SELECT EXISTS(
1299+
SELECT 1
1300+
FROM "PROJECT" AS "PARENT_PROJECT"
1301+
INNER JOIN "PROJECT_HIERARCHY"
1302+
ON "PROJECT_HIERARCHY"."PARENT_PROJECT_ID" = "PARENT_PROJECT"."ID"
1303+
INNER JOIN "PROJECT" AS "CHILD_PROJECT"
1304+
ON "CHILD_PROJECT"."ID" = "PROJECT_HIERARCHY"."CHILD_PROJECT_ID"
1305+
WHERE "PARENT_PROJECT"."ID" = ?
1306+
AND "CHILD_PROJECT"."INACTIVE_SINCE" IS NULL
1307+
AND "PROJECT_HIERARCHY"."DEPTH" > 0
1308+
)
1309+
""");
1310+
query.setParameters(project.getId());
1311+
try {
1312+
return (boolean) query.executeUnique();
1313+
} finally {
1314+
query.closeAll();
13321315
}
1333-
return hasActiveChild;
13341316
}
13351317

13361318
private List<ProjectVersion> getProjectVersions(Project project) {

src/main/java/org/dependencytrack/persistence/QueryManager.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -566,10 +566,6 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final
566566
return getProjectQueryManager().getProjectsWithoutDescendantsOf(name, excludeInactive, project);
567567
}
568568

569-
public List<UUID> getParents(final Project project) {
570-
return getProjectQueryManager().getParents(project);
571-
}
572-
573569
public boolean hasAccess(final Principal principal, final Project project) {
574570
return getProjectQueryManager().hasAccess(principal, project);
575571
}
@@ -781,10 +777,6 @@ public List<Policy> getAllPolicies() {
781777
return getPolicyQueryManager().getAllPolicies();
782778
}
783779

784-
public List<Policy> getApplicablePolicies(final Project project) {
785-
return getPolicyQueryManager().getApplicablePolicies(project);
786-
}
787-
788780
public Policy getPolicy(final String name) {
789781
return getPolicyQueryManager().getPolicy(name);
790782
}

src/main/resources/migration/changelog-v5.6.0.xml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,4 +636,125 @@
636636
onDelete="CASCADE" onUpdate="NO ACTION" referencedColumnNames="ID"
637637
referencedTableName="WORKFLOW_STATE" validate="true"/>
638638
</changeSet>
639+
640+
<changeSet id="v5.6.0-13" author="nscuro">
641+
<createTable tableName="PROJECT_HIERARCHY">
642+
<column name="PARENT_PROJECT_ID" type="BIGINT"/>
643+
<column name="CHILD_PROJECT_ID" type="BIGINT"/>
644+
<column name="DEPTH" type="SMALLINT">
645+
<constraints nullable="false"/>
646+
</column>
647+
</createTable>
648+
649+
<addPrimaryKey
650+
tableName="PROJECT_HIERARCHY"
651+
columnNames="PARENT_PROJECT_ID, CHILD_PROJECT_ID"
652+
constraintName="PROJECT_HIERARCHY_PK"/>
653+
<addForeignKeyConstraint
654+
baseTableName="PROJECT_HIERARCHY"
655+
baseColumnNames="PARENT_PROJECT_ID"
656+
constraintName="PROJECT_HIERARCHY_PARENT_PROJECT_FK"
657+
referencedTableName="PROJECT"
658+
referencedColumnNames="ID"
659+
deferrable="true"
660+
initiallyDeferred="true"/>
661+
<addForeignKeyConstraint
662+
baseTableName="PROJECT_HIERARCHY"
663+
baseColumnNames="CHILD_PROJECT_ID"
664+
constraintName="PROJECT_HIERARCHY_CHILD_PROJECT_FK"
665+
referencedTableName="PROJECT"
666+
referencedColumnNames="ID"
667+
deferrable="true"
668+
initiallyDeferred="true"/>
669+
670+
<sql splitStatements="false">
671+
CREATE FUNCTION project_hierarchy_maintenance_on_project_insert()
672+
RETURNS TRIGGER AS $$
673+
BEGIN
674+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
675+
VALUES(NEW."ID", NEW."ID", 0);
676+
677+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
678+
SELECT "PARENT_PROJECT_ID", NEW."ID", "DEPTH" + 1
679+
FROM "PROJECT_HIERARCHY"
680+
WHERE "CHILD_PROJECT_ID" = NEW."PARENT_PROJECT_ID";
681+
682+
RETURN NEW;
683+
END;
684+
$$ LANGUAGE plpgsql;
685+
</sql>
686+
687+
<sql splitStatements="false">
688+
CREATE FUNCTION project_hierarchy_maintenance_on_project_update()
689+
RETURNS TRIGGER AS $$
690+
BEGIN
691+
DELETE FROM "PROJECT_HIERARCHY" WHERE "CHILD_PROJECT_ID" = old."ID";
692+
693+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
694+
VALUES (NEW."ID", NEW."ID", 0);
695+
696+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
697+
SELECT "PARENT_PROJECT_ID", NEW."ID", "DEPTH" + 1
698+
FROM "PROJECT_HIERARCHY"
699+
WHERE "CHILD_PROJECT_ID" = NEW."PARENT_PROJECT_ID";
700+
701+
RETURN NEW;
702+
END;
703+
$$ LANGUAGE plpgsql;
704+
</sql>
705+
706+
<sql splitStatements="false">
707+
CREATE FUNCTION project_hierarchy_maintenance_on_project_delete()
708+
RETURNS TRIGGER AS $$
709+
BEGIN
710+
DELETE FROM "PROJECT_HIERARCHY"
711+
WHERE "PARENT_PROJECT_ID" IN (SELECT "ID" FROM old_table)
712+
OR "CHILD_PROJECT_ID" IN (SELECT "ID" FROM old_table);
713+
714+
RETURN NULL;
715+
END;
716+
$$ LANGUAGE plpgsql;
717+
</sql>
718+
719+
<sql splitStatements="true">
720+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_insert
721+
AFTER INSERT ON "PROJECT"
722+
FOR EACH ROW
723+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_insert();
724+
725+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_update
726+
AFTER UPDATE OF "PARENT_PROJECT_ID" ON "PROJECT"
727+
FOR EACH ROW
728+
WHEN (OLD."PARENT_PROJECT_ID" IS DISTINCT FROM NEW."PARENT_PROJECT_ID")
729+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_update();
730+
731+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_delete
732+
AFTER DELETE ON "PROJECT"
733+
REFERENCING OLD TABLE AS old_table
734+
FOR EACH STATEMENT
735+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_delete();
736+
</sql>
737+
738+
<sql splitStatements="true">
739+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
740+
SELECT "ID", "ID", 0 FROM "PROJECT";
741+
742+
WITH RECURSIVE cte_project_hierarchy AS (
743+
SELECT "ID" AS child_id
744+
, "PARENT_PROJECT_ID" AS parent_id
745+
, 1 AS depth
746+
FROM "PROJECT"
747+
UNION ALL
748+
SELECT child_id
749+
, "PARENT_PROJECT_ID" AS parent_id
750+
, depth + 1 AS depth
751+
FROM cte_project_hierarchy
752+
INNER JOIN "PROJECT"
753+
ON "PROJECT"."ID" = cte_project_hierarchy.parent_id
754+
WHERE "PROJECT"."PARENT_PROJECT_ID" IS NOT NULL
755+
)
756+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
757+
SELECT parent_id, child_id, depth FROM cte_project_hierarchy;
758+
</sql>
759+
</changeSet>
639760
</databaseChangeLog>

src/main/resources/migration/procedures/function_has-project-access.sql

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@ create or replace function has_project_access(
77
stable
88
as
99
$$
10-
with recursive project_hierarchy(id, parent_id) as(
11-
select "ID" as id
12-
, "PARENT_PROJECT_ID" as parent_id
13-
from "PROJECT"
14-
where "ID" = project_id
15-
union all
16-
select "PROJECT"."ID" as id
17-
, "PROJECT"."PARENT_PROJECT_ID" as parent_id
18-
from "PROJECT"
19-
inner join project_hierarchy
20-
on project_hierarchy.parent_id = "PROJECT"."ID"
21-
)
2210
select exists(
2311
select 1
24-
from project_hierarchy
25-
inner join "PROJECT_ACCESS_TEAMS"
26-
on "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = project_hierarchy.id
12+
from "PROJECT_ACCESS_TEAMS"
13+
inner join "PROJECT_HIERARCHY"
14+
on "PROJECT_HIERARCHY"."PARENT_PROJECT_ID" = "PROJECT_ACCESS_TEAMS"."PROJECT_ID"
2715
where "PROJECT_ACCESS_TEAMS"."TEAM_ID" = any(team_ids)
16+
and "PROJECT_HIERARCHY"."CHILD_PROJECT_ID" = project_id
2817
)
2918
$$;

0 commit comments

Comments
 (0)