Skip to content

Commit c3270ae

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

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
@@ -1216,37 +1216,6 @@ public PaginatedResult getProjectsWithoutDescendantsOf(final String name, final
12161216
return result;
12171217
}
12181218

1219-
/**
1220-
* Fetch the {@link UUID}s of all parents of a given {@link Project}.
1221-
*
1222-
* @param project The {@link Project} to fetch the parent {@link UUID}s for
1223-
* @return A {@link List} of {@link UUID}s
1224-
*/
1225-
@Override
1226-
public List<UUID> getParents(final Project project) {
1227-
return getParents(project.getUuid(), new ArrayList<>());
1228-
}
1229-
1230-
private List<UUID> getParents(final UUID uuid, final List<UUID> parents) {
1231-
final UUID parentUuid;
1232-
final Query<Project> query = pm.newQuery(Project.class);
1233-
try {
1234-
query.setFilter("uuid == :uuid && parent != null");
1235-
query.setParameters(uuid);
1236-
query.setResult("parent.uuid");
1237-
parentUuid = query.executeResultUnique(UUID.class);
1238-
} finally {
1239-
query.closeAll();
1240-
}
1241-
1242-
if (parentUuid == null) {
1243-
return parents;
1244-
}
1245-
1246-
parents.add(parentUuid);
1247-
return getParents(parentUuid, parents);
1248-
}
1249-
12501219
/**
12511220
* Check whether a {@link Project} with a given {@code name} and {@code version} exists.
12521221
*
@@ -1281,30 +1250,41 @@ public boolean doesProjectExist(final String name, final String version) {
12811250
}
12821251
}
12831252

1284-
private static boolean isChildOf(Project project, UUID uuid) {
1285-
boolean isChild = false;
1286-
if (project.getParent() != null) {
1287-
if (project.getParent().getUuid().equals(uuid)) {
1288-
return true;
1289-
} else {
1290-
isChild = isChildOf(project.getParent(), uuid);
1291-
}
1253+
private boolean isChildOf(Project project, UUID uuid) {
1254+
final Query<?> query = pm.newQuery(Query.SQL, /* language=SQL */ """
1255+
SELECT EXISTS (
1256+
SELECT 1
1257+
FROM "PROJECT_HIERARCHY"
1258+
WHERE "PARENT_PROJECT_ID" = (SELECT "ID" FROM "PROJECT" WHERE "UUID" = ?)
1259+
AND "CHILD_PROJECT_ID" = ?
1260+
)
1261+
""");
1262+
query.setParameters(uuid, project.getId());
1263+
try {
1264+
return (boolean) query.executeUnique();
1265+
} finally {
1266+
query.closeAll();
12921267
}
1293-
return isChild;
12941268
}
12951269

1296-
private static boolean hasActiveChild(Project project) {
1297-
boolean hasActiveChild = false;
1298-
if (project.getChildren() != null) {
1299-
for (Project child : project.getChildren()) {
1300-
if (child.isActive() || hasActiveChild) {
1301-
return true;
1302-
} else {
1303-
hasActiveChild = hasActiveChild(child);
1304-
}
1305-
}
1270+
private boolean hasActiveChild(Project project) {
1271+
final Query<?> query = pm.newQuery(Query.SQL, /* language=SQL */ """
1272+
SELECT EXISTS (
1273+
SELECT 1
1274+
FROM "PROJECT_HIERARCHY" AS hierarchy
1275+
INNER JOIN "PROJECT" AS child_project
1276+
ON child_project."ID" = hierarchy."CHILD_PROJECT_ID"
1277+
WHERE hierarchy."PARENT_PROJECT_ID" = ?
1278+
AND hierarchy."DEPTH" > 0
1279+
AND child_project."INACTIVE_SINCE" IS NULL
1280+
)
1281+
""");
1282+
query.setParameters(project.getId());
1283+
try {
1284+
return (boolean) query.executeUnique();
1285+
} finally {
1286+
query.closeAll();
13061287
}
1307-
return hasActiveChild;
13081288
}
13091289

13101290
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
@@ -674,4 +674,125 @@
674674
<column name="COMPONENT_ID"/>
675675
</createIndex>
676676
</changeSet>
677+
678+
<changeSet id="v5.6.0-14" author="nscuro">
679+
<createTable tableName="PROJECT_HIERARCHY">
680+
<column name="PARENT_PROJECT_ID" type="BIGINT"/>
681+
<column name="CHILD_PROJECT_ID" type="BIGINT"/>
682+
<column name="DEPTH" type="SMALLINT">
683+
<constraints nullable="false"/>
684+
</column>
685+
</createTable>
686+
687+
<addPrimaryKey
688+
tableName="PROJECT_HIERARCHY"
689+
columnNames="PARENT_PROJECT_ID, CHILD_PROJECT_ID"
690+
constraintName="PROJECT_HIERARCHY_PK"/>
691+
<addForeignKeyConstraint
692+
baseTableName="PROJECT_HIERARCHY"
693+
baseColumnNames="PARENT_PROJECT_ID"
694+
constraintName="PROJECT_HIERARCHY_PARENT_PROJECT_FK"
695+
referencedTableName="PROJECT"
696+
referencedColumnNames="ID"
697+
deferrable="true"
698+
initiallyDeferred="true"/>
699+
<addForeignKeyConstraint
700+
baseTableName="PROJECT_HIERARCHY"
701+
baseColumnNames="CHILD_PROJECT_ID"
702+
constraintName="PROJECT_HIERARCHY_CHILD_PROJECT_FK"
703+
referencedTableName="PROJECT"
704+
referencedColumnNames="ID"
705+
deferrable="true"
706+
initiallyDeferred="true"/>
707+
708+
<sql splitStatements="false">
709+
CREATE FUNCTION project_hierarchy_maintenance_on_project_insert()
710+
RETURNS TRIGGER AS $$
711+
BEGIN
712+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
713+
VALUES(NEW."ID", NEW."ID", 0);
714+
715+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
716+
SELECT "PARENT_PROJECT_ID", NEW."ID", "DEPTH" + 1
717+
FROM "PROJECT_HIERARCHY"
718+
WHERE "CHILD_PROJECT_ID" = NEW."PARENT_PROJECT_ID";
719+
720+
RETURN NEW;
721+
END;
722+
$$ LANGUAGE plpgsql;
723+
</sql>
724+
725+
<sql splitStatements="false">
726+
CREATE FUNCTION project_hierarchy_maintenance_on_project_update()
727+
RETURNS TRIGGER AS $$
728+
BEGIN
729+
DELETE FROM "PROJECT_HIERARCHY" WHERE "CHILD_PROJECT_ID" = old."ID";
730+
731+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
732+
VALUES (NEW."ID", NEW."ID", 0);
733+
734+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
735+
SELECT "PARENT_PROJECT_ID", NEW."ID", "DEPTH" + 1
736+
FROM "PROJECT_HIERARCHY"
737+
WHERE "CHILD_PROJECT_ID" = NEW."PARENT_PROJECT_ID";
738+
739+
RETURN NEW;
740+
END;
741+
$$ LANGUAGE plpgsql;
742+
</sql>
743+
744+
<sql splitStatements="false">
745+
CREATE FUNCTION project_hierarchy_maintenance_on_project_delete()
746+
RETURNS TRIGGER AS $$
747+
BEGIN
748+
DELETE FROM "PROJECT_HIERARCHY"
749+
WHERE "PARENT_PROJECT_ID" IN (SELECT "ID" FROM old_table)
750+
OR "CHILD_PROJECT_ID" IN (SELECT "ID" FROM old_table);
751+
752+
RETURN NULL;
753+
END;
754+
$$ LANGUAGE plpgsql;
755+
</sql>
756+
757+
<sql splitStatements="true">
758+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_insert
759+
AFTER INSERT ON "PROJECT"
760+
FOR EACH ROW
761+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_insert();
762+
763+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_update
764+
AFTER UPDATE OF "PARENT_PROJECT_ID" ON "PROJECT"
765+
FOR EACH ROW
766+
WHEN (OLD."PARENT_PROJECT_ID" IS DISTINCT FROM NEW."PARENT_PROJECT_ID")
767+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_update();
768+
769+
CREATE TRIGGER trigger_project_hierarchy_maintenance_on_project_delete
770+
AFTER DELETE ON "PROJECT"
771+
REFERENCING OLD TABLE AS old_table
772+
FOR EACH STATEMENT
773+
EXECUTE FUNCTION project_hierarchy_maintenance_on_project_delete();
774+
</sql>
775+
776+
<sql splitStatements="true">
777+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
778+
SELECT "ID", "ID", 0 FROM "PROJECT";
779+
780+
WITH RECURSIVE cte_project_hierarchy AS (
781+
SELECT "ID" AS child_id
782+
, "PARENT_PROJECT_ID" AS parent_id
783+
, 1 AS depth
784+
FROM "PROJECT"
785+
UNION ALL
786+
SELECT child_id
787+
, "PARENT_PROJECT_ID" AS parent_id
788+
, depth + 1 AS depth
789+
FROM cte_project_hierarchy
790+
INNER JOIN "PROJECT"
791+
ON "PROJECT"."ID" = cte_project_hierarchy.parent_id
792+
WHERE "PROJECT"."PARENT_PROJECT_ID" IS NOT NULL
793+
)
794+
INSERT INTO "PROJECT_HIERARCHY" ("PARENT_PROJECT_ID", "CHILD_PROJECT_ID", "DEPTH")
795+
SELECT parent_id, child_id, depth FROM cte_project_hierarchy;
796+
</sql>
797+
</changeSet>
677798
</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)