Skip to content

Commit 273b9b6

Browse files
authored
Merge pull request #17 from JavaWebStack/TimothyGillespie/addQueryTestTooling
Add query test tooling
2 parents fedf906 + c4bbe4f commit 273b9b6

File tree

6 files changed

+440
-0
lines changed

6 files changed

+440
-0
lines changed

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@
7979
<version>5.4.2</version>
8080
<scope>test</scope>
8181
</dependency>
82+
<dependency>
83+
<groupId>org.apache.commons</groupId>
84+
<artifactId>commons-lang3</artifactId>
85+
<version>3.5</version>
86+
<scope>test</scope>
87+
</dependency>
88+
<dependency>
89+
<groupId>org.projectlombok</groupId>
90+
<artifactId>lombok</artifactId>
91+
<version>1.18.16</version>
92+
<scope>test</scope>
93+
</dependency>
8294
</dependencies>
8395

8496
<distributionManagement>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package org.javawebstack.orm.test.other;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import org.apache.commons.lang3.RandomStringUtils;
6+
import org.javawebstack.orm.test.shared.util.QueryStringUtil;
7+
import org.junit.jupiter.api.Test;
8+
9+
import java.util.*;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
13+
14+
/*
15+
* The scope of this class is to test the QueryUtility as it will be used to write other tests.
16+
*/
17+
class QueryUtilTest {
18+
19+
@Test
20+
void testGetSectionSimple() {
21+
List<SectionRecord> list = new LinkedList<>(Arrays.asList(
22+
new SectionRecord("SELECT", RandomStringUtils.randomAlphanumeric(3, 12)),
23+
new SectionRecord("FROM", RandomStringUtils.randomAlphanumeric(3, 10)),
24+
new SectionRecord("WHERE", RandomStringUtils.randomAlphanumeric(3, 10)),
25+
new SectionRecord("ORDER BY", RandomStringUtils.randomAlphanumeric(3, 10)),
26+
new SectionRecord("GROUP BY", RandomStringUtils.randomAlphanumeric(3, 10)),
27+
new SectionRecord("HAVING", RandomStringUtils.randomAlphanumeric(3, 10)),
28+
new SectionRecord("LIMIT", RandomStringUtils.randomNumeric(1, 100)),
29+
new SectionRecord("OFFSET", RandomStringUtils.randomNumeric(1, 100))
30+
));
31+
32+
this.performStandardTestOnList(list);
33+
34+
}
35+
36+
@Test
37+
void testCasingDoesNotMatter() {
38+
List<SectionRecord> list = new LinkedList<>(Arrays.asList(
39+
new SectionRecord("select", RandomStringUtils.randomAlphanumeric(3, 12)),
40+
new SectionRecord("fRom", RandomStringUtils.randomAlphanumeric(3, 10)),
41+
new SectionRecord("where", RandomStringUtils.randomAlphanumeric(3, 10)),
42+
new SectionRecord("ordEr by", RandomStringUtils.randomAlphanumeric(3, 10)),
43+
new SectionRecord("Group By", RandomStringUtils.randomAlphanumeric(3, 10)),
44+
new SectionRecord("hAvIng", RandomStringUtils.randomAlphanumeric(3, 10)),
45+
new SectionRecord("limit", RandomStringUtils.randomNumeric(1, 100)),
46+
new SectionRecord("offset", RandomStringUtils.randomNumeric(1, 100))
47+
));
48+
49+
this.performStandardTestOnList(list);
50+
}
51+
52+
// Example from here: https://www.freecodecamp.org/news/sql-example/
53+
@Test
54+
void testGetUsualCase() {
55+
List<SectionRecord> list = new LinkedList<>(Arrays.asList(
56+
new SectionRecord("SELECT", "`Customers`.`CustomerName`, `Orders`.`OrderID`"),
57+
new SectionRecord("FROM", "`Customers`"),
58+
new SectionRecord("FULL OUTER JOIN", "`Orders` ON `Customers`.`CustomerID`=`Orders`.`CustomerID`"),
59+
new SectionRecord("ORDER BY", "`Customers`.`CustomerName`")
60+
));
61+
62+
this.performStandardTestOnList(list);
63+
}
64+
65+
@Test
66+
void testTrapExpressionsWhichAreEscaped() {
67+
List<SectionRecord> list = new LinkedList<>(Arrays.asList(
68+
new SectionRecord("SELECT", "`FROM`.`SELECT`, `GROUP BY`.`having`"),
69+
new SectionRecord("FROM", "`order by`"),
70+
new SectionRecord("WHERE", "`from` LIKE `where` AND 'from' = 'where'"),
71+
new SectionRecord("FULL OUTER JOIN", "`from` ON `FROM`.`SELECT`=`select`.`from`"),
72+
new SectionRecord("ORDER BY", "`limit`.`where`")
73+
));
74+
75+
this.performStandardTestOnList(list);
76+
}
77+
78+
@Test
79+
void testMultipleOccurrences() {
80+
List<SectionRecord> list = new LinkedList<>(Arrays.asList(
81+
new SectionRecord("SELECT", RandomStringUtils.randomAlphanumeric(3, 12)),
82+
new SectionRecord("FROM", RandomStringUtils.randomAlphanumeric(3, 12)),
83+
new SectionRecord("JOIN", RandomStringUtils.randomAlphanumeric(3, 12)),
84+
new SectionRecord("JOIN", RandomStringUtils.randomAlphanumeric(3, 12)),
85+
new SectionRecord("JOIN", RandomStringUtils.randomAlphanumeric(3, 12))
86+
));
87+
88+
String queryString = this.getQueryStringFromList(list);
89+
QueryStringUtil util = new QueryStringUtil(queryString);
90+
91+
SectionRecord currentRecord = list.get(0);
92+
assertEquals(currentRecord.getValue(), util.getTopLevelSectionsByKeyword(currentRecord.getKey()).get(0));
93+
94+
currentRecord = list.get(1);
95+
assertEquals(currentRecord.getValue(), util.getTopLevelSectionsByKeyword(currentRecord.getKey()).get(0));
96+
97+
currentRecord = list.get(2);
98+
assertEquals(currentRecord.getValue(), util.getTopLevelSectionsByKeyword(currentRecord.getKey()).get(0));
99+
100+
currentRecord = list.get(3);
101+
assertEquals(currentRecord.getValue(), util.getTopLevelSectionsByKeyword(currentRecord.getKey()).get(1));
102+
103+
currentRecord = list.get(4);
104+
assertEquals(currentRecord.getValue(), util.getTopLevelSectionsByKeyword(currentRecord.getKey()).get(2));
105+
106+
}
107+
/*
108+
* Boilerplate Code Reduction Methods
109+
*/
110+
111+
private String getQueryStringFromList(List<SectionRecord> list) {
112+
StringBuilder builder = new StringBuilder();
113+
114+
for(SectionRecord entry : list)
115+
builder
116+
.append(entry.getKey())
117+
.append(" ")
118+
.append(entry.getValue())
119+
.append(" ");
120+
121+
return builder.toString().trim();
122+
}
123+
124+
/*
125+
* The standard test in this case will be that each section cnly exists once and as defined per list.
126+
*/
127+
private void performStandardTestOnList(List<SectionRecord> list) {
128+
String query = this.getQueryStringFromList(list);
129+
QueryStringUtil verification = new QueryStringUtil(query);
130+
131+
for (SectionRecord entry : list) {
132+
List<String> foundSections = verification.getTopLevelSectionsByKeyword(entry.getKey());
133+
String firstSection = foundSections.get(0);
134+
assertEquals(1, foundSections.size(), "More than one section or no section was found, but only one unique section was expected.");
135+
assertEquals(
136+
entry.getValue(),
137+
firstSection,
138+
String.format(
139+
"The section name %s has been %s instead of %s.",
140+
entry.getKey(),
141+
firstSection,
142+
entry.getValue()
143+
)
144+
);
145+
}
146+
}
147+
148+
@Getter
149+
@AllArgsConstructor
150+
static class SectionRecord {
151+
String key;
152+
String value;
153+
}
154+
155+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package org.javawebstack.orm.test.shared.knowledge;
2+
3+
import java.util.Arrays;
4+
import java.util.HashSet;
5+
6+
/**
7+
* The QueryKnowledgeBase serves as a decentralized information container around raw query terms.
8+
*/
9+
public class QueryKnowledgeBase {
10+
11+
/**
12+
* Top level select keyword are SQL keywords which occur in SELECT statements and do not depend on another keyword
13+
* except for SELECT and FROM (which are both included in this set as well).
14+
* For example JOIN can appear after FROM statement so it is included. The ON keyword depends on a JOIN keyword though
15+
* which we view as a sub keyword of JOIN and therefore not as a top level keyword.
16+
*/
17+
public static final HashSet<String> TOP_LEVEL_SELECT_KEYWORDS;
18+
19+
/**
20+
* Quote characters are characters which prevents an SQL parser from picking up on a keyword, if the
21+
* wrap the keyword.
22+
*/
23+
public static final HashSet<Character> QUOTE_CHARACTERS;
24+
25+
26+
static {
27+
TOP_LEVEL_SELECT_KEYWORDS = new HashSet<>(Arrays.asList(
28+
"SELECT",
29+
"FROM",
30+
"WHERE",
31+
"ORDER BY",
32+
"JOIN",
33+
"JOIN LEFT",
34+
"JOIN RIGHT",
35+
"INNER JOIN",
36+
"FULL JOIN",
37+
"FULL OUTER JOIN",
38+
"OUTER JOIN",
39+
"GROUP BY",
40+
"HAVING",
41+
"LIMIT",
42+
"OFFSET"
43+
));
44+
45+
QUOTE_CHARACTERS = new HashSet<>(Arrays.asList(
46+
'`',
47+
'\''
48+
));
49+
}
50+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.javawebstack.orm.test.shared.setup;
2+
3+
import org.javawebstack.orm.Model;
4+
import org.javawebstack.orm.ORM;
5+
import org.javawebstack.orm.Repo;
6+
import org.javawebstack.orm.exception.ORMConfigurationException;
7+
import org.javawebstack.orm.test.shared.settings.MySQLConnectionContainer;
8+
9+
public class ModelSetup extends MySQLConnectionContainer {
10+
11+
public static <T extends Model> Repo<T> setUpModel(Class<T> clazz) {
12+
13+
// Converting to Runtime exception to avoid having to declare the thrown error which has no utility
14+
try {
15+
ORM.register(clazz, sql());
16+
} catch (ORMConfigurationException e) {
17+
throw new RuntimeException(e);
18+
}
19+
20+
return Repo.get(clazz);
21+
}
22+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.javawebstack.orm.test.shared.util;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
import java.util.LinkedList;
7+
import java.util.List;
8+
import java.util.Locale;
9+
10+
import static org.javawebstack.orm.test.shared.knowledge.QueryKnowledgeBase.QUOTE_CHARACTERS;
11+
import static org.javawebstack.orm.test.shared.knowledge.QueryKnowledgeBase.TOP_LEVEL_SELECT_KEYWORDS;
12+
import static org.junit.jupiter.api.Assertions.fail;
13+
14+
/**
15+
* The QueryStringUtil class wraps a normal string which is assumed, but not verified, to be an SQL query and provides
16+
* various utility functions for tests purposes on them.
17+
*/
18+
public class QueryStringUtil {
19+
20+
String queryString;
21+
22+
public QueryStringUtil(String queryString) {
23+
this.queryString = queryString;
24+
}
25+
26+
27+
/**
28+
* Retrieves all sections of the query prefaced by the given top level keyword. Does not include the keyword and
29+
* is delimited by the next top level keyword occurrence if it exists.
30+
* @param topLevelKeyword The top level SQL select query keyword to look for.
31+
* @return A list of strings containing only the inner part of the query. It will include additional spaces if the
32+
* query had more than one space.
33+
*/
34+
public List<String> getTopLevelSectionsByKeyword(String topLevelKeyword) {
35+
String capitalizedKeyword = topLevelKeyword.toUpperCase(Locale.ROOT);
36+
String queryString = this.queryString;
37+
38+
List<String> sections = new LinkedList<String>();
39+
40+
SectionInfo sectionInfo;
41+
do {
42+
sectionInfo = this.getNextTopLevelSectionByKeyword(queryString, topLevelKeyword);
43+
if (sectionInfo != null) {
44+
sections.add(sectionInfo.getSectionString());
45+
queryString = queryString.substring(sectionInfo.getEndIndex());
46+
}
47+
} while (sectionInfo != null);
48+
49+
return sections;
50+
}
51+
private SectionInfo getNextTopLevelSectionByKeyword(String queryString, String topLevelKeyword) {
52+
String capitalizedKeyword = topLevelKeyword.toUpperCase(Locale.ROOT);
53+
String capitalizedQueryString = queryString.toUpperCase(Locale.ROOT);
54+
55+
if(!TOP_LEVEL_SELECT_KEYWORDS.contains(capitalizedKeyword))
56+
fail(String.format("Section name %s is not supported.", capitalizedKeyword));
57+
58+
boolean insideQuote = false;
59+
char lastQuoteChar = ' ';
60+
int startIndex = -1;
61+
int endIndex = -1;
62+
63+
char currentCharacter;
64+
65+
66+
for (int i = 0; i < queryString.length(); i++) {
67+
currentCharacter = queryString.charAt(i);
68+
69+
if (insideQuote) {
70+
if (lastQuoteChar == currentCharacter)
71+
insideQuote = false;
72+
73+
continue;
74+
}
75+
76+
if (QUOTE_CHARACTERS.contains(currentCharacter)) {
77+
insideQuote = true;
78+
lastQuoteChar = currentCharacter;
79+
continue;
80+
}
81+
82+
if ((queryString.length() - i) <= capitalizedKeyword.length())
83+
break;
84+
85+
if (
86+
startIndex == -1 &&
87+
String.valueOf(currentCharacter).equalsIgnoreCase(String.valueOf(capitalizedKeyword.charAt(0))) &&
88+
capitalizedQueryString.substring(i).startsWith(capitalizedKeyword)
89+
) {
90+
// +1 skips the white space after the section name
91+
startIndex = i + capitalizedKeyword.length() + 1;
92+
// The -1 is to counteract the increment after the loop
93+
i = startIndex - 1;
94+
95+
continue;
96+
}
97+
98+
if (startIndex != -1 && this.checkQueryStringStartsWithSectionName(capitalizedQueryString.substring(i))) {
99+
endIndex = i - 1;
100+
break;
101+
}
102+
}
103+
104+
if (startIndex == -1) {
105+
return null;
106+
}
107+
108+
if (endIndex == -1)
109+
return new SectionInfo(queryString.substring(startIndex), startIndex, queryString.length());
110+
else
111+
return new SectionInfo(queryString.substring(startIndex, endIndex), startIndex, endIndex);
112+
}
113+
114+
115+
public boolean checkQueryStringStartsWithSectionName(String partialQueryString) {
116+
for (String singleSectionName : TOP_LEVEL_SELECT_KEYWORDS)
117+
if(partialQueryString.toUpperCase(Locale.ROOT).startsWith(singleSectionName))
118+
return true;
119+
120+
return false;
121+
}
122+
123+
@Getter
124+
@AllArgsConstructor
125+
private static class SectionInfo {
126+
String sectionString;
127+
int startIndex;
128+
int endIndex;
129+
}
130+
}

0 commit comments

Comments
 (0)