Skip to content

Commit

Permalink
feat: QUALIFY clause
Browse files Browse the repository at this point in the history
  • Loading branch information
manticore-projects committed Jun 15, 2023
1 parent 996ebd9 commit 75e4d30
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ jobs:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
uses: gradle/gradle-build-action@v2.4.2
with:
arguments: check
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ Assertions.assertEquals("b", b.getColumnName());

**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)).

## Alternatives to JSqlParser?
[**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.

## [Documentation](https://jsqlparser.github.io/JSqlParser)

### [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#parse-a-sql-statements)
Expand Down
28 changes: 7 additions & 21 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {

id "ca.coglinc2.javacc" version "latest.release"
id 'jacoco'
id 'com.github.kt3k.coveralls' version "latest.release"
id "com.github.spotbugs" version "latest.release"
id "com.diffplug.spotless" version "latest.release"
id 'pmd'
Expand Down Expand Up @@ -179,6 +180,10 @@ test {
maxHeapSize = "1G"
}

coveralls {
jacocoReportPath 'build/reports/jacoco/test/jacocoTestReport.xml'
}

jacocoTestReport {
dependsOn test // tests are required to run before generating the report
reports {
Expand Down Expand Up @@ -272,9 +277,7 @@ spotbugs {
}

pmd {
consoleOutput = false
//toolVersion = "6.46.0"

consoleOutput = true
sourceSets = [sourceSets.main]

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

// doFirst() {
// exec {
// args = [
// "install"
// , "sphinx_rtd_theme"
// , "sphinx-book-theme"
// , "myst_parser"
// , "sphinx-prompt"
// , "sphinx_substitution_extensions"
// , "sphinx_issues"
// , "sphinx_inline_tabs"
// , "pygments"
// ]
// executable "pip"
// }
// }

String PROLOG = """
.. |_| unicode:: U+00A0
:trim:
Expand Down Expand Up @@ -555,7 +541,7 @@ publishing {
maven {
name = "GitHubPackages"

url = uri("https://maven.pkg.github.com/manticore-projects/jsqlparser")
url = uri("https://maven.pkg.github.com/JSQLParser/jsqlparser")
credentials(PasswordCredentials)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class ParserKeywordsUtils {
{"FORCE", RESTRICTED_SQL2016}, {"FOREIGN", RESTRICTED_SQL2016},
{"FROM", RESTRICTED_SQL2016}, {"FULL", RESTRICTED_SQL2016},
{"GROUP", RESTRICTED_SQL2016}, {"GROUPING", RESTRICTED_ALIAS},
{"QUALIFY", RESTRICTED_ALIAS},
{"HAVING", RESTRICTED_SQL2016}, {"IF", RESTRICTED_SQL2016}, {"IIF", RESTRICTED_ALIAS},
{"IGNORE", RESTRICTED_ALIAS}, {"ILIKE", RESTRICTED_SQL2016}, {"IN", RESTRICTED_SQL2016},
{"INNER", RESTRICTED_SQL2016}, {"INTERSECT", RESTRICTED_SQL2016},
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class PlainSelect extends Select {
private Expression where;
private GroupByElement groupBy;
private Expression having;
private Expression qualify;
private OptimizeFor optimizeFor;
private Skip skip;
private boolean mySqlHintStraightJoin;
Expand Down Expand Up @@ -274,6 +275,15 @@ public void setHaving(Expression expression) {
having = expression;
}

public Expression getQualify() {
return qualify;
}

public PlainSelect setQualify(Expression qualify) {
this.qualify = qualify;
return this;
}

/**
* A list of {@link Expression}s of the GROUP BY clause. It is null in case there is no GROUP BY
* clause
Expand Down Expand Up @@ -465,6 +475,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) {
if (having != null) {
builder.append(" HAVING ").append(having);
}
if (qualify != null) {
builder.append(" QUALIFY ").append(qualify);
}
if (windowDefinitions != null) {
builder.append(" WINDOW ");
builder.append(windowDefinitions.stream().map(WindowDefinition::toString)
Expand Down
125 changes: 125 additions & 0 deletions src/main/java/net/sf/jsqlparser/util/PerformanceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package net.sf.jsqlparser.util;

import net.sf.jsqlparser.parser.CCJSqlParser;

public class PerformanceTest {
@SuppressWarnings("PMD.ExcessiveMethodLength")
public static void main(String[] args) throws Exception {
String sqlStr = "SELECT e.id\n" +
" , e.code\n" +
" , e.review_type\n" +
" , e.review_object\n" +
" , e.review_first_datetime AS reviewfirsttime\n" +
" , e.review_latest_datetime AS reviewnewtime\n" +
" , e.risk_event\n" +
" , e.risk_detail\n" +
" , e.risk_grade\n" +
" , e.risk_status\n" +
" , If( e.deal_type IS NULL\n" +
" OR e.deal_type = '', '--', e.deal_type ) AS dealtype\n" +
" , e.deal_result\n" +
" , If( e.deal_remark IS NULL\n" +
" OR e.deal_remark = '', '--', e.deal_remark ) AS dealremark\n" +
" , e.is_deleted\n" +
" , e.review_object_id\n" +
" , e.archive_id\n" +
" , e.feature AS featurename\n" +
" , Ifnull( ( SELECT real_name\n" +
" FROM bladex.blade_user\n" +
" WHERE id = e.review_first_user ), ( SELECT DISTINCT\n" +
" real_name\n" +
" FROM app_sys.asys_uniapp_rn_auth\n"
+
" WHERE uniapp_user_id = e.review_first_user\n"
+
" AND is_disable = 0 ) ) AS reviewfirstuser\n"
+
" , Ifnull( ( SELECT real_name\n" +
" FROM bladex.blade_user\n" +
" WHERE id = e.review_latest_user ), ( SELECT DISTINCT\n" +
" real_name\n" +
" FROM app_sys.asys_uniapp_rn_auth\n"
+
" WHERE uniapp_user_id = e.review_latest_user\n"
+
" AND is_disable = 0 ) ) AS reviewnewuser\n"
+
" , If( ( SELECT real_name\n" +
" FROM bladex.blade_user\n" +
" WHERE id = e.deal_user ) IS NOT NULL\n" +
" AND e.deal_user != - 9999, ( SELECT real_name\n" +
" FROM bladex.blade_user\n" +
" WHERE id = e.deal_user ), '--' ) AS dealuser\n"
+
" , CASE\n" +
" WHEN 'COMPANY'\n" +
" THEN Concat( ( SELECT ar.customer_name\n" +
" FROM mtp_cs.mtp_rsk_cust_archive ar\n" +
" WHERE ar.is_deleted = 0\n" +
" AND ar.id = e.archive_id ), If( ( SELECT alias\n"
+
" FROM web_crm.wcrm_customer\n"
+
" WHERE id = e.customer_id ) = ''\n"
+
" OR ( SELECT alias\n" +
" FROM web_crm.wcrm_customer\n" +
" WHERE id = e.customer_id ) IS NULL, ' ', Concat( '(', ( SELECT alias\n"
+
" FROM web_crm.wcrm_customer\n"
+
" WHERE id = e.customer_id ), ')' ) ) )\n"
+
" WHEN 'EMPLOYEE'\n" +
" THEN ( SELECT Concat( auth.real_name, ' ', auth.phone )\n" +
" FROM app_sys.asys_uniapp_rn_auth auth\n" +
" WHERE auth.is_disable = 0\n" +
" AND auth.uniapp_user_id = e.uniapp_user_id )\n" +
" WHEN 'DEAL'\n" +
" THEN ( SELECT DISTINCT\n" +
" Concat( batch.code, '-', detail.line_seq\n" +
" , ' ', Ifnull( ( SELECT DISTINCT\n" +
" auth.real_name\n" +
" FROM app_sys.asys_uniapp_rn_auth auth\n"
+
" WHERE auth.uniapp_user_id = e.uniapp_user_id\n"
+
" AND auth.is_disable = 0 ), ' ' ) )\n"
+
" FROM web_pym.wpym_payment_batch_detail detail\n" +
" LEFT JOIN web_pym.wpym_payment_batch batch\n" +
" ON detail.payment_batch_id = batch.id\n" +
" WHERE detail.id = e.review_object_id )\n" +
" WHEN 'TASK'\n" +
" THEN ( SELECT code\n" +
" FROM web_tm.wtm_task task\n" +
" WHERE e.review_object_id = task.id )\n" +
" ELSE NULL\n" +
" END AS reviewobjectname\n" +
" , CASE\n" +
" WHEN 4\n" +
" THEN 'HIGH_LEVEL'\n" +
" WHEN 3\n" +
" THEN 'MEDIUM_LEVEL'\n" +
" WHEN 2\n" +
" THEN 'LOW_LEVEL'\n" +
" ELSE 'HEALTHY'\n" +
" END AS risklevel\n" +
"FROM mtp_cs.mtp_rsk_event e\n" +
"WHERE e.is_deleted = 0\n" +
"ORDER BY e.review_latest_datetime DESC\n" +
"LIMIT 30\n" +
";";

long startMillis = System.currentTimeMillis();
for (int i = 1; i < 1000; i++) {
final CCJSqlParser parser = new CCJSqlParser(sqlStr)
.withSquareBracketQuotation(false)
.withAllowComplexParsing(true)
.withBackslashEscapeCharacter(false);
parser.Statements();
long endMillis = System.currentTimeMillis();
System.out.println("Duration [ms]: " + (endMillis - startMillis) / i);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ public void visit(PlainSelect plainSelect) {
buffer.append(" HAVING ");
plainSelect.getHaving().accept(expressionVisitor);
}
if (plainSelect.getQualify() != null) {
buffer.append(" QUALIFY ");
plainSelect.getQualify().accept(expressionVisitor);
}
if (plainSelect.getWindowDefinitions() != null) {
buffer.append(" WINDOW ");
buffer.append(plainSelect.getWindowDefinitions().stream()
Expand Down
18 changes: 17 additions & 1 deletion src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */
| <K_PROCEDURE:"PROCEDURE">
| <K_PUBLIC:"PUBLIC">
| <K_PURGE:"PURGE">
| <K_QUALIFY: "QUALIFY">
| <K_QUERY:"QUERY">
| <K_QUICK : "QUICK">
| <K_QUIESCE: "QUIESCE">
Expand Down Expand Up @@ -1742,7 +1743,9 @@ String RelObjectName() :
{
(result = RelObjectNameWithoutValue()
| tk=<K_GROUP> | tk=<K_INTERVAL> | tk=<K_ON> | tk=<K_START> | tk=<K_TOP> | tk=<K_VALUE>
| tk=<K_VALUES> | tk=<K_CREATE> | tk=<K_TABLES> | tk=<K_CONNECT> | tk=<K_IGNORE > )
| tk=<K_VALUES> | tk=<K_CREATE> | tk=<K_TABLES> | tk=<K_CONNECT> | tk=<K_IGNORE >
| tk=<K_QUALIFY>
)

{ return tk!=null ? tk.image : result; }
}
Expand Down Expand Up @@ -2019,6 +2022,7 @@ PlainSelect PlainSelect() #PlainSelect:
List<OrderByElement> orderByElements;
GroupByElement groupBy = null;
Expression having = null;
Expression qualify;
Limit limitBy = null;
Limit limit = null;
Offset offset = null;
Expand Down Expand Up @@ -2089,6 +2093,7 @@ PlainSelect PlainSelect() #PlainSelect:
[ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }]
[ LOOKAHEAD(2) groupBy=GroupByColumnReferences() { plainSelect.setGroupByElement(groupBy); }]
[ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); }]
[ LOOKAHEAD(2) qualify=Qualify() {plainSelect.setQualify(qualify); }]
[ LOOKAHEAD(2) forClause = ForClause() {plainSelect.setForClause(forClause);} ]
[ LOOKAHEAD(<K_ORDER> <K_SIBLINGS> <K_BY>) orderByElements = OrderByElements() { plainSelect.setOracleSiblings(true); plainSelect.setOrderByElements(orderByElements); } ]
[ LOOKAHEAD(2) <K_WINDOW>
Expand Down Expand Up @@ -2828,6 +2833,17 @@ Expression Having():
}
}

Expression Qualify():
{
Expression qualify = null;
}
{
<K_QUALIFY> qualify=Expression()
{
return qualify;
}
}

List<OrderByElement> OrderByElements():
{
List<OrderByElement> orderByList = new ArrayList<OrderByElement>();
Expand Down
2 changes: 2 additions & 0 deletions src/site/sphinx/keywords.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and
+----------------------+-------------+-----------+
| GROUPING | Yes | |
+----------------------+-------------+-----------+
| QUALIFY | Yes | |
+----------------------+-------------+-----------+
| HAVING | Yes | Yes |
+----------------------+-------------+-----------+
| IF | Yes | Yes |
Expand Down
12 changes: 11 additions & 1 deletion src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
Expand Down Expand Up @@ -3370,7 +3371,7 @@ public void testTableFunctionWithParams() throws Exception {

// verify params
assertNotNull(function.getParameters());
List<Expression> expressions = function.getParameters().getExpressions();
ExpressionList<?> expressions = function.getParameters();
assertEquals(2, expressions.size());

Expression firstParam = expressions.get(0);
Expand Down Expand Up @@ -5700,4 +5701,13 @@ void testArrayColumnsIssue1757() throws JSQLParserException {
sqlStr = "SELECT cast(my_map['my_key'] as int) FROM my_table WHERE id = 123";
assertSqlCanBeParsedAndDeparsed(sqlStr, true);
}

@Test
void testQualifyClauseIssue1805() throws JSQLParserException {
String sqlStr = "SELECT i, p, o\n" +
" FROM qt\n" +
" QUALIFY ROW_NUMBER() OVER (PARTITION BY p ORDER BY o) = 1";

TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
}
}
Loading

0 comments on commit 75e4d30

Please sign in to comment.