Skip to content

Commit 02d8515

Browse files
authored
feat: adding Table filterDFS functionaility (#21)
Implementing the same tests as we have in the Go [plugin-sdk](https://github.com/cloudquery/plugin-sdk/blob/main/schema/table_test.go#L58-L276) supporting filtering tables by including and skipping tables based on glob patterns.
1 parent b9b73d1 commit 02d8515

File tree

8 files changed

+412
-22
lines changed

8 files changed

+412
-22
lines changed

Diff for: lib/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ dependencies {
3434
implementation "io.grpc:grpc-testing:1.57.1"
3535
implementation "io.cloudquery:plugin-pb-java:0.0.5"
3636
implementation "org.apache.arrow:arrow-vector:12.0.1"
37+
38+
testImplementation 'org.assertj:assertj-core:3.24.2'
3739
}
3840

3941
testing {
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.cloudquery.helper;
2+
3+
import lombok.Getter;
4+
5+
import java.nio.file.FileSystems;
6+
import java.nio.file.Path;
7+
import java.nio.file.PathMatcher;
8+
9+
public class GlobMatcher {
10+
private final PathMatcher pathMatcher;
11+
12+
@Getter
13+
private final String stringMatch;
14+
15+
public GlobMatcher(String stringMatch) {
16+
this.stringMatch = stringMatch;
17+
this.pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + stringMatch);
18+
}
19+
20+
public boolean matches(String name) {
21+
return pathMatcher.matches(Path.of(name));
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.cloudquery.schema;
2+
3+
public class SchemaException extends Exception {
4+
public SchemaException() {
5+
}
6+
7+
public SchemaException(String message) {
8+
super(message);
9+
}
10+
11+
public SchemaException(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
15+
public SchemaException(Throwable cause) {
16+
super(cause);
17+
}
18+
19+
public SchemaException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
20+
super(message, cause, enableSuppression, writableStackTrace);
21+
}
22+
}

Diff for: lib/src/main/java/io/cloudquery/schema/Table.java

+91
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package io.cloudquery.schema;
22

3+
import io.cloudquery.helper.GlobMatcher;
34
import lombok.Builder;
45
import lombok.Getter;
56

7+
import java.util.ArrayList;
68
import java.util.Collections;
79
import java.util.HashMap;
810
import java.util.List;
911
import java.util.Map;
12+
import java.util.Optional;
13+
import java.util.function.Predicate;
1014

1115
@Builder(toBuilder = true)
1216
@Getter
@@ -23,6 +27,67 @@ public static List<Table> flattenTables(List<Table> tables) {
2327
return flattenMap.values().stream().toList();
2428
}
2529

30+
public static List<Table> filterDFS(List<Table> tables, List<String> includeConfiguration, List<String> skipConfiguration, boolean skipDependentTables) throws SchemaException {
31+
List<GlobMatcher> includes = includeConfiguration.stream().map(GlobMatcher::new).toList();
32+
List<GlobMatcher> excludes = skipConfiguration.stream().map(GlobMatcher::new).toList();
33+
34+
List<Table> flattenedTables = flattenTables(tables);
35+
for (GlobMatcher includeMatcher : includes) {
36+
boolean includeMatch = false;
37+
for (Table table : flattenedTables) {
38+
if (includeMatcher.matches(table.getName())) {
39+
includeMatch = true;
40+
break;
41+
}
42+
}
43+
if (!includeMatch) {
44+
throw new SchemaException("table configuration includes a pattern \"" + includeMatcher.getStringMatch() + "\" with no matches");
45+
}
46+
}
47+
for (GlobMatcher excludeMatcher : excludes) {
48+
boolean excludeMatch = false;
49+
for (Table table : flattenedTables) {
50+
if (excludeMatcher.matches(table.getName())) {
51+
excludeMatch = true;
52+
break;
53+
}
54+
}
55+
if (!excludeMatch) {
56+
throw new SchemaException("skip configuration includes a pattern \"" + excludeMatcher.getStringMatch() + "\" with no matches");
57+
}
58+
}
59+
60+
Predicate<Table> include = table -> {
61+
for (GlobMatcher matcher : includes) {
62+
if (matcher.matches(table.getName())) {
63+
return true;
64+
}
65+
}
66+
return false;
67+
};
68+
69+
Predicate<Table> exclude = table -> {
70+
for (GlobMatcher matcher : excludes) {
71+
if (matcher.matches(table.getName())) {
72+
return true;
73+
}
74+
}
75+
return false;
76+
};
77+
78+
return filterDFSFunc(tables, include, exclude, skipDependentTables);
79+
}
80+
81+
private static List<Table> filterDFSFunc(List<Table> tables, Predicate<Table> include, Predicate<Table> exclude, boolean skipDependentTables) {
82+
List<Table> filteredTables = new ArrayList<>();
83+
for (Table table : tables) {
84+
Table filteredTable = table.toBuilder().parent(null).build();
85+
Optional<Table> optionalFilteredTable = filteredTable.filterDfs(false, include, exclude, skipDependentTables);
86+
optionalFilteredTable.ifPresent(filteredTables::add);
87+
}
88+
return filteredTables;
89+
}
90+
2691
public static int maxDepth(List<Table> tables) {
2792
int depth = 0;
2893
if (tables.isEmpty()) {
@@ -39,6 +104,32 @@ public static int maxDepth(List<Table> tables) {
39104

40105
private String name;
41106

107+
private Table parent;
108+
42109
@Builder.Default
43110
private List<Table> relations = Collections.emptyList();
111+
112+
private Optional<Table> filterDfs(boolean parentMatched, Predicate<Table> include, Predicate<Table> exclude, boolean skipDependentTables) {
113+
if (exclude.test(this)) {
114+
return Optional.empty();
115+
}
116+
boolean matched = parentMatched && !skipDependentTables;
117+
if (include.test(this)) {
118+
matched = true;
119+
}
120+
List<Table> filteredRelations = new ArrayList<>();
121+
for (Table relation : relations) {
122+
Optional<Table> filteredChild = relation.filterDfs(matched, include, exclude, skipDependentTables);
123+
if (filteredChild.isPresent()) {
124+
matched = true;
125+
filteredRelations.add(filteredChild.get());
126+
}
127+
}
128+
this.relations = filteredRelations;
129+
if (matched) {
130+
return Optional.of(this);
131+
}
132+
return Optional.empty();
133+
}
134+
44135
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.cloudquery.helper;
2+
3+
import org.junit.Test;
4+
5+
import static org.junit.Assert.assertFalse;
6+
import static org.junit.Assert.assertTrue;
7+
8+
public class GlobMatcherTest {
9+
@Test
10+
public void shouldMatchWildcard() {
11+
GlobMatcher globMatcher = new GlobMatcher("*");
12+
13+
assertTrue(globMatcher.matches("aws_ec2_vpc"));
14+
assertTrue(globMatcher.matches("aws_ec2_eip"));
15+
assertTrue(globMatcher.matches("aws_ec2_instance"));
16+
}
17+
18+
@Test
19+
public void shouldMatchWildcardSuffix() {
20+
GlobMatcher globMatcher = new GlobMatcher("aws_*");
21+
22+
assertTrue(globMatcher.matches("aws_ec2_vpc"));
23+
assertTrue(globMatcher.matches("aws_ec2_eip"));
24+
assertTrue(globMatcher.matches("aws_ec2_instance"));
25+
26+
assertFalse(globMatcher.matches("gcp_project"));
27+
assertFalse(globMatcher.matches("other_aws_resource"));
28+
}
29+
30+
@Test
31+
public void shouldMatchWildcardPrefixAndSuffix() {
32+
GlobMatcher globMatcher = new GlobMatcher("*ec2*");
33+
34+
assertTrue(globMatcher.matches("aws_ec2_vpc"));
35+
assertTrue(globMatcher.matches("aws_ec2_eip"));
36+
assertTrue(globMatcher.matches("aws_ec2_instance"));
37+
38+
assertFalse(globMatcher.matches("gcp_project"));
39+
assertFalse(globMatcher.matches("other_aws_resource"));
40+
}
41+
}

0 commit comments

Comments
 (0)