Skip to content

Commit 75e4d30

Browse files
feat: QUALIFY clause
- fixes #1805
1 parent 996ebd9 commit 75e4d30

File tree

11 files changed

+368
-24
lines changed

11 files changed

+368
-24
lines changed

.github/workflows/gradle.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ jobs:
2929
java-version: '11'
3030
distribution: 'temurin'
3131
- name: Build with Gradle
32-
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
32+
uses: gradle/gradle-build-action@v2.4.2
3333
with:
3434
arguments: check

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ Assertions.assertEquals("b", b.getColumnName());
6161

6262
**JSqlParser** can also be used to create SQL Statements from Java Code with a fluent API (see [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#build-a-sql-statements)).
6363

64+
## Alternatives to JSqlParser?
65+
[**General SQL Parser**](http://www.sqlparser.com/features/introduce.php?utm_source=github-jsqlparser&utm_medium=text-general) looks pretty good, with extended SQL syntax (like PL/SQL and T-SQL) and java + .NET APIs. The tool is commercial (license available online), with a free download option.
66+
6467
## [Documentation](https://jsqlparser.github.io/JSqlParser)
6568

6669
### [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#parse-a-sql-statements)

build.gradle

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ plugins {
77

88
id "ca.coglinc2.javacc" version "latest.release"
99
id 'jacoco'
10+
id 'com.github.kt3k.coveralls' version "latest.release"
1011
id "com.github.spotbugs" version "latest.release"
1112
id "com.diffplug.spotless" version "latest.release"
1213
id 'pmd'
@@ -179,6 +180,10 @@ test {
179180
maxHeapSize = "1G"
180181
}
181182

183+
coveralls {
184+
jacocoReportPath 'build/reports/jacoco/test/jacocoTestReport.xml'
185+
}
186+
182187
jacocoTestReport {
183188
dependsOn test // tests are required to run before generating the report
184189
reports {
@@ -272,9 +277,7 @@ spotbugs {
272277
}
273278

274279
pmd {
275-
consoleOutput = false
276-
//toolVersion = "6.46.0"
277-
280+
consoleOutput = true
278281
sourceSets = [sourceSets.main]
279282

280283
// clear the ruleset in order to use configured rules only
@@ -436,23 +439,6 @@ xslt {
436439
tasks.register('sphinx', Exec) {
437440
dependsOn(gitChangelogTask, renderRR, xslt, updateKeywords, xmldoc)
438441

439-
// doFirst() {
440-
// exec {
441-
// args = [
442-
// "install"
443-
// , "sphinx_rtd_theme"
444-
// , "sphinx-book-theme"
445-
// , "myst_parser"
446-
// , "sphinx-prompt"
447-
// , "sphinx_substitution_extensions"
448-
// , "sphinx_issues"
449-
// , "sphinx_inline_tabs"
450-
// , "pygments"
451-
// ]
452-
// executable "pip"
453-
// }
454-
// }
455-
456442
String PROLOG = """
457443
.. |_| unicode:: U+00A0
458444
:trim:
@@ -555,7 +541,7 @@ publishing {
555541
maven {
556542
name = "GitHubPackages"
557543

558-
url = uri("https://maven.pkg.github.com/manticore-projects/jsqlparser")
544+
url = uri("https://maven.pkg.github.com/JSQLParser/jsqlparser")
559545
credentials(PasswordCredentials)
560546
}
561547
}

src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class ParserKeywordsUtils {
5959
{"FORCE", RESTRICTED_SQL2016}, {"FOREIGN", RESTRICTED_SQL2016},
6060
{"FROM", RESTRICTED_SQL2016}, {"FULL", RESTRICTED_SQL2016},
6161
{"GROUP", RESTRICTED_SQL2016}, {"GROUPING", RESTRICTED_ALIAS},
62+
{"QUALIFY", RESTRICTED_ALIAS},
6263
{"HAVING", RESTRICTED_SQL2016}, {"IF", RESTRICTED_SQL2016}, {"IIF", RESTRICTED_ALIAS},
6364
{"IGNORE", RESTRICTED_ALIAS}, {"ILIKE", RESTRICTED_SQL2016}, {"IN", RESTRICTED_SQL2016},
6465
{"INNER", RESTRICTED_SQL2016}, {"INTERSECT", RESTRICTED_SQL2016},

src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class PlainSelect extends Select {
3838
private Expression where;
3939
private GroupByElement groupBy;
4040
private Expression having;
41+
private Expression qualify;
4142
private OptimizeFor optimizeFor;
4243
private Skip skip;
4344
private boolean mySqlHintStraightJoin;
@@ -274,6 +275,15 @@ public void setHaving(Expression expression) {
274275
having = expression;
275276
}
276277

278+
public Expression getQualify() {
279+
return qualify;
280+
}
281+
282+
public PlainSelect setQualify(Expression qualify) {
283+
this.qualify = qualify;
284+
return this;
285+
}
286+
277287
/**
278288
* A list of {@link Expression}s of the GROUP BY clause. It is null in case there is no GROUP BY
279289
* clause
@@ -465,6 +475,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
465475
if (having != null) {
466476
builder.append(" HAVING ").append(having);
467477
}
478+
if (qualify != null) {
479+
builder.append(" QUALIFY ").append(qualify);
480+
}
468481
if (windowDefinitions != null) {
469482
builder.append(" WINDOW ");
470483
builder.append(windowDefinitions.stream().map(WindowDefinition::toString)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package net.sf.jsqlparser.util;
2+
3+
import net.sf.jsqlparser.parser.CCJSqlParser;
4+
5+
public class PerformanceTest {
6+
@SuppressWarnings("PMD.ExcessiveMethodLength")
7+
public static void main(String[] args) throws Exception {
8+
String sqlStr = "SELECT e.id\n" +
9+
" , e.code\n" +
10+
" , e.review_type\n" +
11+
" , e.review_object\n" +
12+
" , e.review_first_datetime AS reviewfirsttime\n" +
13+
" , e.review_latest_datetime AS reviewnewtime\n" +
14+
" , e.risk_event\n" +
15+
" , e.risk_detail\n" +
16+
" , e.risk_grade\n" +
17+
" , e.risk_status\n" +
18+
" , If( e.deal_type IS NULL\n" +
19+
" OR e.deal_type = '', '--', e.deal_type ) AS dealtype\n" +
20+
" , e.deal_result\n" +
21+
" , If( e.deal_remark IS NULL\n" +
22+
" OR e.deal_remark = '', '--', e.deal_remark ) AS dealremark\n" +
23+
" , e.is_deleted\n" +
24+
" , e.review_object_id\n" +
25+
" , e.archive_id\n" +
26+
" , e.feature AS featurename\n" +
27+
" , Ifnull( ( SELECT real_name\n" +
28+
" FROM bladex.blade_user\n" +
29+
" WHERE id = e.review_first_user ), ( SELECT DISTINCT\n" +
30+
" real_name\n" +
31+
" FROM app_sys.asys_uniapp_rn_auth\n"
32+
+
33+
" WHERE uniapp_user_id = e.review_first_user\n"
34+
+
35+
" AND is_disable = 0 ) ) AS reviewfirstuser\n"
36+
+
37+
" , Ifnull( ( SELECT real_name\n" +
38+
" FROM bladex.blade_user\n" +
39+
" WHERE id = e.review_latest_user ), ( SELECT DISTINCT\n" +
40+
" real_name\n" +
41+
" FROM app_sys.asys_uniapp_rn_auth\n"
42+
+
43+
" WHERE uniapp_user_id = e.review_latest_user\n"
44+
+
45+
" AND is_disable = 0 ) ) AS reviewnewuser\n"
46+
+
47+
" , If( ( SELECT real_name\n" +
48+
" FROM bladex.blade_user\n" +
49+
" WHERE id = e.deal_user ) IS NOT NULL\n" +
50+
" AND e.deal_user != - 9999, ( SELECT real_name\n" +
51+
" FROM bladex.blade_user\n" +
52+
" WHERE id = e.deal_user ), '--' ) AS dealuser\n"
53+
+
54+
" , CASE\n" +
55+
" WHEN 'COMPANY'\n" +
56+
" THEN Concat( ( SELECT ar.customer_name\n" +
57+
" FROM mtp_cs.mtp_rsk_cust_archive ar\n" +
58+
" WHERE ar.is_deleted = 0\n" +
59+
" AND ar.id = e.archive_id ), If( ( SELECT alias\n"
60+
+
61+
" FROM web_crm.wcrm_customer\n"
62+
+
63+
" WHERE id = e.customer_id ) = ''\n"
64+
+
65+
" OR ( SELECT alias\n" +
66+
" FROM web_crm.wcrm_customer\n" +
67+
" WHERE id = e.customer_id ) IS NULL, ' ', Concat( '(', ( SELECT alias\n"
68+
+
69+
" FROM web_crm.wcrm_customer\n"
70+
+
71+
" WHERE id = e.customer_id ), ')' ) ) )\n"
72+
+
73+
" WHEN 'EMPLOYEE'\n" +
74+
" THEN ( SELECT Concat( auth.real_name, ' ', auth.phone )\n" +
75+
" FROM app_sys.asys_uniapp_rn_auth auth\n" +
76+
" WHERE auth.is_disable = 0\n" +
77+
" AND auth.uniapp_user_id = e.uniapp_user_id )\n" +
78+
" WHEN 'DEAL'\n" +
79+
" THEN ( SELECT DISTINCT\n" +
80+
" Concat( batch.code, '-', detail.line_seq\n" +
81+
" , ' ', Ifnull( ( SELECT DISTINCT\n" +
82+
" auth.real_name\n" +
83+
" FROM app_sys.asys_uniapp_rn_auth auth\n"
84+
+
85+
" WHERE auth.uniapp_user_id = e.uniapp_user_id\n"
86+
+
87+
" AND auth.is_disable = 0 ), ' ' ) )\n"
88+
+
89+
" FROM web_pym.wpym_payment_batch_detail detail\n" +
90+
" LEFT JOIN web_pym.wpym_payment_batch batch\n" +
91+
" ON detail.payment_batch_id = batch.id\n" +
92+
" WHERE detail.id = e.review_object_id )\n" +
93+
" WHEN 'TASK'\n" +
94+
" THEN ( SELECT code\n" +
95+
" FROM web_tm.wtm_task task\n" +
96+
" WHERE e.review_object_id = task.id )\n" +
97+
" ELSE NULL\n" +
98+
" END AS reviewobjectname\n" +
99+
" , CASE\n" +
100+
" WHEN 4\n" +
101+
" THEN 'HIGH_LEVEL'\n" +
102+
" WHEN 3\n" +
103+
" THEN 'MEDIUM_LEVEL'\n" +
104+
" WHEN 2\n" +
105+
" THEN 'LOW_LEVEL'\n" +
106+
" ELSE 'HEALTHY'\n" +
107+
" END AS risklevel\n" +
108+
"FROM mtp_cs.mtp_rsk_event e\n" +
109+
"WHERE e.is_deleted = 0\n" +
110+
"ORDER BY e.review_latest_datetime DESC\n" +
111+
"LIMIT 30\n" +
112+
";";
113+
114+
long startMillis = System.currentTimeMillis();
115+
for (int i = 1; i < 1000; i++) {
116+
final CCJSqlParser parser = new CCJSqlParser(sqlStr)
117+
.withSquareBracketQuotation(false)
118+
.withAllowComplexParsing(true)
119+
.withBackslashEscapeCharacter(false);
120+
parser.Statements();
121+
long endMillis = System.currentTimeMillis();
122+
System.out.println("Duration [ms]: " + (endMillis - startMillis) / i);
123+
}
124+
}
125+
}

src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ public void visit(PlainSelect plainSelect) {
252252
buffer.append(" HAVING ");
253253
plainSelect.getHaving().accept(expressionVisitor);
254254
}
255+
if (plainSelect.getQualify() != null) {
256+
buffer.append(" QUALIFY ");
257+
plainSelect.getQualify().accept(expressionVisitor);
258+
}
255259
if (plainSelect.getWindowDefinitions() != null) {
256260
buffer.append(" WINDOW ");
257261
buffer.append(plainSelect.getWindowDefinitions().stream()

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
346346
| <K_PROCEDURE:"PROCEDURE">
347347
| <K_PUBLIC:"PUBLIC">
348348
| <K_PURGE:"PURGE">
349+
| <K_QUALIFY: "QUALIFY">
349350
| <K_QUERY:"QUERY">
350351
| <K_QUICK : "QUICK">
351352
| <K_QUIESCE: "QUIESCE">
@@ -1742,7 +1743,9 @@ String RelObjectName() :
17421743
{
17431744
(result = RelObjectNameWithoutValue()
17441745
| tk=<K_GROUP> | tk=<K_INTERVAL> | tk=<K_ON> | tk=<K_START> | tk=<K_TOP> | tk=<K_VALUE>
1745-
| tk=<K_VALUES> | tk=<K_CREATE> | tk=<K_TABLES> | tk=<K_CONNECT> | tk=<K_IGNORE > )
1746+
| tk=<K_VALUES> | tk=<K_CREATE> | tk=<K_TABLES> | tk=<K_CONNECT> | tk=<K_IGNORE >
1747+
| tk=<K_QUALIFY>
1748+
)
17461749

17471750
{ return tk!=null ? tk.image : result; }
17481751
}
@@ -2019,6 +2022,7 @@ PlainSelect PlainSelect() #PlainSelect:
20192022
List<OrderByElement> orderByElements;
20202023
GroupByElement groupBy = null;
20212024
Expression having = null;
2025+
Expression qualify;
20222026
Limit limitBy = null;
20232027
Limit limit = null;
20242028
Offset offset = null;
@@ -2089,6 +2093,7 @@ PlainSelect PlainSelect() #PlainSelect:
20892093
[ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }]
20902094
[ LOOKAHEAD(2) groupBy=GroupByColumnReferences() { plainSelect.setGroupByElement(groupBy); }]
20912095
[ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }]
2096+
[ LOOKAHEAD(2) qualify=Qualify() {plainSelect.setQualify(qualify); }]
20922097
[ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ]
20932098
[ LOOKAHEAD(<K_ORDER> <K_SIBLINGS> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOracleSiblings(true); plainSelect.setOrderByElements(orderByElements); } ]
20942099
[ LOOKAHEAD(2) <K_WINDOW>
@@ -2828,6 +2833,17 @@ Expression Having():
28282833
}
28292834
}
28302835

2836+
Expression Qualify():
2837+
{
2838+
Expression qualify = null;
2839+
}
2840+
{
2841+
<K_QUALIFY> qualify=Expression()
2842+
{
2843+
return qualify;
2844+
}
2845+
}
2846+
28312847
List<OrderByElement> OrderByElements():
28322848
{
28332849
List<OrderByElement> orderByList = new ArrayList<OrderByElement>();

src/site/sphinx/keywords.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and
6565
+----------------------+-------------+-----------+
6666
| GROUPING | Yes | |
6767
+----------------------+-------------+-----------+
68+
| QUALIFY | Yes | |
69+
+----------------------+-------------+-----------+
6870
| HAVING | Yes | Yes |
6971
+----------------------+-------------+-----------+
7072
| IF | Yes | Yes |

src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
3232
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
3333
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
34+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
3435
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
3536
import net.sf.jsqlparser.expression.operators.relational.InExpression;
3637
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
@@ -3370,7 +3371,7 @@ public void testTableFunctionWithParams() throws Exception {
33703371

33713372
// verify params
33723373
assertNotNull(function.getParameters());
3373-
List<Expression> expressions = function.getParameters().getExpressions();
3374+
ExpressionList<?> expressions = function.getParameters();
33743375
assertEquals(2, expressions.size());
33753376

33763377
Expression firstParam = expressions.get(0);
@@ -5700,4 +5701,13 @@ void testArrayColumnsIssue1757() throws JSQLParserException {
57005701
sqlStr = "SELECT cast(my_map['my_key'] as int) FROM my_table WHERE id = 123";
57015702
assertSqlCanBeParsedAndDeparsed(sqlStr, true);
57025703
}
5704+
5705+
@Test
5706+
void testQualifyClauseIssue1805() throws JSQLParserException {
5707+
String sqlStr = "SELECT i, p, o\n" +
5708+
" FROM qt\n" +
5709+
" QUALIFY ROW_NUMBER() OVER (PARTITION BY p ORDER BY o) = 1";
5710+
5711+
TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
5712+
}
57035713
}

0 commit comments

Comments
 (0)