Skip to content

Commit e593c67

Browse files
committed
Materialize project hierarchies in separate table
Signed-off-by: nscuro <nscuro@protonmail.com> # Conflicts: # src/main/resources/migration/changelog-v5.6.0.xml # src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java
1 parent c66cf10 commit e593c67

File tree

8 files changed

+453
-211
lines changed

8 files changed

+453
-211
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: 31 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,37 +1253,6 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final
12531253
return result;
12541254
}
12551255

1256-
/**
1257-
* Fetch the {@link UUID}s of all parents of a given {@link Project}.
1258-
*
1259-
* @param project The {@link Project} to fetch the parent {@link UUID}s for
1260-
* @return A {@link List} of {@link UUID}s
1261-
*/
1262-
@Override
1263-
public List<UUID> getParents(final Project project) {
1264-
return getParents(project.getUuid(), new ArrayList<>());
1265-
}
1266-
1267-
private List<UUID> getParents(final UUID uuid, final List<UUID> parents) {
1268-
final UUID parentUuid;
1269-
final Query<Project> query = pm.newQuery(Project.class);
1270-
try {
1271-
query.setFilter("uuid == :uuid && parent != null");
1272-
query.setParameters(uuid);
1273-
query.setResult("parent.uuid");
1274-
parentUuid = query.executeResultUnique(UUID.class);
1275-
} finally {
1276-
query.closeAll();
1277-
}
1278-
1279-
if (parentUuid == null) {
1280-
return parents;
1281-
}
1282-
1283-
parents.add(parentUuid);
1284-
return getParents(parentUuid, parents);
1285-
}
1286-
12871256
/**
12881257
* Check whether a {@link Project} with a given {@code name} and {@code version} exists.
12891258
*
@@ -1318,30 +1287,41 @@ public boolean doesProjectExist(final String name, final String version) {
13181287
}
13191288
}
13201289

1321-
private static boolean isChildOf(Project project, UUID uuid) {
1322-
boolean isChild = false;
1323-
if (project.getParent() != null) {
1324-
if (project.getParent().getUuid().equals(uuid)) {
1325-
return true;
1326-
} else {
1327-
isChild = isChildOf(project.getParent(), uuid);
1328-
}
1290+
private boolean isChildOf(Project project, UUID uuid) {
1291+
final Query<?> query = pm.newQuery(Query.SQL, /* language=SQL */ """
1292+
SELECT EXISTS (
1293+
SELECT 1
1294+
FROM "PROJECT_HIERARCHY"
1295+
WHERE "PARENT_PROJECT_ID" = (SELECT "ID" FROM "PROJECT" WHERE "UUID" = ?)
1296+
AND "CHILD_PROJECT_ID" = ?
1297+
)
1298+
""");
1299+
query.setParameters(uuid, project.getId());
1300+
try {
1301+
return (boolean) query.executeUnique();
1302+
} finally {
1303+
query.closeAll();
13291304
}
1330-
return isChild;
13311305
}
13321306

1333-
private static boolean hasActiveChild(Project project) {
1334-
boolean hasActiveChild = false;
1335-
if (project.getChildren() != null) {
1336-
for (Project child : project.getChildren()) {
1337-
if (child.isActive() || hasActiveChild) {
1338-
return true;
1339-
} else {
1340-
hasActiveChild = hasActiveChild(child);
1341-
}
1342-
}
1307+
private boolean hasActiveChild(Project project) {
1308+
final Query<?> query = pm.newQuery(Query.SQL, /* language=SQL */ """
1309+
SELECT EXISTS (
1310+
SELECT 1
1311+
FROM "PROJECT_HIERARCHY" AS hierarchy
1312+
INNER JOIN "PROJECT" AS child_project
1313+
ON child_project."ID" = hierarchy."CHILD_PROJECT_ID"
1314+
WHERE hierarchy."PARENT_PROJECT_ID" = ?
1315+
AND hierarchy."DEPTH" > 0
1316+
AND child_project."INACTIVE_SINCE" IS NULL
1317+
)
1318+
""");
1319+
query.setParameters(project.getId());
1320+
try {
1321+
return (boolean) query.executeUnique();
1322+
} finally {
1323+
query.closeAll();
13431324
}
1344-
return hasActiveChild;
13451325
}
13461326

13471327
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
@@ -567,10 +567,6 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final
567567
return getProjectQueryManager().getProjectsWithoutDescendantsOf(name, excludeInactive, project);
568568
}
569569

570-
public List<UUID> getParents(final Project project) {
571-
return getProjectQueryManager().getParents(project);
572-
}
573-
574570
public boolean hasAccess(final Principal principal, final Project project) {
575571
return getProjectQueryManager().hasAccess(principal, project);
576572
}
@@ -738,10 +734,6 @@ public List<Policy> getAllPolicies() {
738734
return getPolicyQueryManager().getAllPolicies();
739735
}
740736

741-
public List<Policy> getApplicablePolicies(final Project project) {
742-
return getPolicyQueryManager().getApplicablePolicies(project);
743-
}
744-
745737
public Policy getPolicy(final String name) {
746738
return getPolicyQueryManager().getPolicy(name);
747739
}

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

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,4 +702,125 @@
702702
columnNames="PROJECT_ID, TEAM_ID"
703703
constraintName="PROJECT_ACCESS_TEAMS_PK"/>
704704
</changeSet>
705+
706+
<changeSet id="v5.6.0-15" author="nscuro">
707+
<createTable tableName="PROJECT_HIERARCHY">
708+
<column name="PARENT_PROJECT_ID" type="BIGINT"/>
709+
<column name="CHILD_PROJECT_ID" type="BIGINT"/>
710+
<column name="DEPTH" type="SMALLINT">
711+
<constraints nullable="false"/>
712+
</column>
713+
</createTable>
714+
715+
<addPrimaryKey
716+
tableName="PROJECT_HIERARCHY"
717+
columnNames="PARENT_PROJECT_ID, CHILD_PROJECT_ID"
718+
constraintName="PROJECT_HIERARCHY_PK"/>
719+
<addForeignKeyConstraint
720+
baseTableName="PROJECT_HIERARCHY"
721+
baseColumnNames="PARENT_PROJECT_ID"
722+
constraintName="PROJECT_HIERARCHY_PARENT_PROJECT_FK"
723+
referencedTableName="PROJECT"
724+
referencedColumnNames="ID"
725+
deferrable="true"
726+
initiallyDeferred="true"/>
727+
<addForeignKeyConstraint
728+
baseTableName="PROJECT_HIERARCHY"
729+
baseColumnNames="CHILD_PROJECT_ID"
730+
constraintName="PROJECT_HIERARCHY_CHILD_PROJECT_FK"
731+
referencedTableName="PROJECT"
732+
referencedColumnNames="ID"
733+
deferrable="true"
734+
initiallyDeferred="true"/>
735+
736+
<sql splitStatements="false">
737+
CREATE FUNCTION project_hierarchy_maintenance_on_project_insert()
738+
RETURNS TRIGGER AS $$
739+
BEGIN
740+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
741+
VALUES(NEW."ID", NEW."ID", 0);
742+
743+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
744+
SELECT "PARENT_PROJECT_ID", NEW."ID", "DEPTH" + 1
745+
FROM "PROJECT_HIERARCHY"
746+
WHERE "CHILD_PROJECT_ID" = NEW."PARENT_PROJECT_ID";
747+
748+
RETURN NEW;
749+
END;
750+
$$ LANGUAGE plpgsql;
751+
</sql>
752+
753+
<sql splitStatements="false">
754+
CREATE FUNCTION project_hierarchy_maintenance_on_project_update()
755+
RETURNS TRIGGER AS $$
756+
BEGIN
757+
DELETE FROM "PROJECT_HIERARCHY" WHERE "CHILD_PROJECT_ID" = old."ID";
758+
759+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
760+
VALUES (NEW."ID", NEW."ID", 0);
761+
762+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
763+
SELECT "PARENT_PROJECT_ID", NEW."ID", "DEPTH" + 1
764+
FROM "PROJECT_HIERARCHY"
765+
WHERE "CHILD_PROJECT_ID" = NEW."PARENT_PROJECT_ID";
766+
767+
RETURN NEW;
768+
END;
769+
$$ LANGUAGE plpgsql;
770+
</sql>
771+
772+
<sql splitStatements="false">
773+
CREATE FUNCTION project_hierarchy_maintenance_on_project_delete()
774+
RETURNS TRIGGER AS $$
775+
BEGIN
776+
DELETE FROM "PROJECT_HIERARCHY"
777+
WHERE "PARENT_PROJECT_ID" IN (SELECT "ID" FROM old_table)
778+
OR "CHILD_PROJECT_ID" IN (SELECT "ID" FROM old_table);
779+
780+
RETURN NULL;
781+
END;
782+
$$ LANGUAGE plpgsql;
783+
</sql>
784+
785+
<sql splitStatements="true">
786+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_insert
787+
AFTER INSERT ON "PROJECT"
788+
FOR EACH ROW
789+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_insert();
790+
791+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_update
792+
AFTER UPDATE OF "PARENT_PROJECT_ID" ON "PROJECT"
793+
FOR EACH ROW
794+
WHEN (OLD."PARENT_PROJECT_ID" IS DISTINCT FROM NEW."PARENT_PROJECT_ID")
795+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_update();
796+
797+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_delete
798+
AFTER DELETE ON "PROJECT"
799+
REFERENCING OLD TABLE AS old_table
800+
FOR EACH STATEMENT
801+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_delete();
802+
</sql>
803+
804+
<sql splitStatements="true">
805+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
806+
SELECT "ID", "ID", 0 FROM "PROJECT";
807+
808+
WITH RECURSIVE cte_project_hierarchy AS (
809+
SELECT "ID" AS child_id
810+
, "PARENT_PROJECT_ID" AS parent_id
811+
, 1 AS depth
812+
FROM "PROJECT"
813+
UNION ALL
814+
SELECT child_id
815+
, "PARENT_PROJECT_ID" AS parent_id
816+
, depth + 1 AS depth
817+
FROM cte_project_hierarchy
818+
INNER JOIN "PROJECT"
819+
ON "PROJECT"."ID" = cte_project_hierarchy.parent_id
820+
WHERE "PROJECT"."PARENT_PROJECT_ID" IS NOT NULL
821+
)
822+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
823+
SELECT parent_id, child_id, depth FROM cte_project_hierarchy;
824+
</sql>
825+
</changeSet>
705826
</databaseChangeLog>
Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,18 @@
1-
create or replace function has_project_access(
2-
project_id bigint
3-
, team_ids bigint[]
4-
) returns bool
5-
language "sql"
6-
parallel safe
7-
stable
8-
as
1+
CREATE OR REPLACE FUNCTION has_project_access(
2+
project_id BIGINT
3+
, team_ids BIGINT[]
4+
) RETURNS BOOL
5+
LANGUAGE "sql"
6+
PARALLEL SAFE
7+
STABLE
8+
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-
)
22-
select exists(
23-
select 1
24-
from project_hierarchy
25-
inner join "PROJECT_ACCESS_TEAMS"
26-
on "PROJECT_ACCESS_TEAMS"."PROJECT_ID" = project_hierarchy.id
27-
where "PROJECT_ACCESS_TEAMS"."TEAM_ID" = any(team_ids)
10+
SELECT EXISTS(
11+
SELECT 1
12+
FROM "PROJECT_ACCESS_TEAMS"
13+
INNER JOIN "PROJECT_HIERARCHY"
14+
ON "PROJECT_HIERARCHY"."PARENT_PROJECT_ID" = "PROJECT_ACCESS_TEAMS"."PROJECT_ID"
15+
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)