Skip to content

Commit 78cff6b

Browse files
authored
Support Doris distribution hints and share join keyword rendering (#2584)
1 parent 1bef513 commit 78cff6b

7 files changed

Lines changed: 196 additions & 29 deletions

File tree

‎src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public enum Dialect {
4646
AdjacentStringLiterals.WHITESPACE,
4747
Feature.allowDoubleQuotedStrings,
4848
Feature.allowBackslashEscapeCharacter), SNOWFLAKE(
49-
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER;
49+
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS;
5050

5151
private final Set<Feature> lexerFeatures;
5252
private final AdjacentStringLiterals adjacentStringLiterals;

‎src/main/java/net/sf/jsqlparser/statement/select/Join.java‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,27 @@ public Join setJoinHint(JoinHint joinHint) {
464464
return this;
465465
}
466466

467+
/** Appends the join keyword, hint and FETCH modifier, followed by a space. */
468+
public StringBuilder appendJoinKeywordTo(StringBuilder builder) {
469+
if (isStraight()) {
470+
builder.append("STRAIGHT_JOIN ");
471+
} else if (isApply()) {
472+
builder.append("APPLY ");
473+
} else {
474+
if (joinHint != null && joinHint.getPosition() == JoinHint.Position.BEFORE_JOIN) {
475+
builder.append(joinHint).append(' ');
476+
}
477+
builder.append("JOIN ");
478+
if (joinHint != null && joinHint.getPosition() == JoinHint.Position.AFTER_JOIN) {
479+
builder.append(joinHint).append(' ');
480+
}
481+
if (fetch) {
482+
builder.append("FETCH ");
483+
}
484+
}
485+
return builder;
486+
}
487+
467488
@Override
468489
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
469490
public String toString() {
@@ -510,19 +531,7 @@ public String toString() {
510531
builder.append("ARRAY ");
511532
}
512533

513-
if (isStraight()) {
514-
builder.append("STRAIGHT_JOIN ");
515-
} else if (isApply()) {
516-
builder.append("APPLY ");
517-
} else {
518-
if (joinHint != null) {
519-
builder.append(joinHint).append(" ");
520-
}
521-
builder.append("JOIN ");
522-
if (fetch) {
523-
builder.append("FETCH ");
524-
}
525-
}
534+
appendJoinKeywordTo(builder);
526535

527536
builder.append(fromItem).append((joinWindow != null) ? " WITHIN " + joinWindow : "");
528537
}

‎src/main/java/net/sf/jsqlparser/statement/select/JoinHint.java‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,43 @@
99
*/
1010
package net.sf.jsqlparser.statement.select;
1111

12+
import java.util.Objects;
13+
1214
/**
13-
* Hints (Transact-SQL) - Join
15+
* SQL Server join hints precede JOIN; Doris distribution hints follow it in square brackets.
1416
*
1517
* @link <a href=
1618
* "https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-join?view=sql-server-ver16">Hints
1719
* (Transact-SQL) - Join</a>
1820
*/
1921

2022
public class JoinHint {
23+
public enum Position {
24+
BEFORE_JOIN, AFTER_JOIN
25+
}
26+
2127
private final String keyword;
28+
private final Position position;
2229

2330
public JoinHint(String keyword) {
31+
this(keyword, Position.BEFORE_JOIN);
32+
}
33+
34+
public JoinHint(String keyword, Position position) {
2435
this.keyword = keyword;
36+
this.position = Objects.requireNonNull(position, "position");
37+
}
38+
39+
public String getKeyword() {
40+
return keyword;
41+
}
42+
43+
public Position getPosition() {
44+
return position;
2545
}
2646

2747
@Override
2848
public String toString() {
29-
return keyword;
49+
return position == Position.AFTER_JOIN ? "[" + keyword + "]" : keyword;
3050
}
3151
}

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

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -765,19 +765,8 @@ public void deparseJoin(Join join) {
765765
builder.append(" ARRAY");
766766
}
767767

768-
if (join.isStraight()) {
769-
builder.append(" STRAIGHT_JOIN ");
770-
} else if (join.isApply()) {
771-
builder.append(" APPLY ");
772-
} else {
773-
if (join.getJoinHint() != null) {
774-
builder.append(" ").append(join.getJoinHint());
775-
}
776-
builder.append(" JOIN ");
777-
if (join.isFetch()) {
778-
builder.append("FETCH ");
779-
}
780-
}
768+
builder.append(' ');
769+
join.appendJoinKeywordTo(builder);
781770

782771
}
783772

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7352,6 +7352,21 @@ JoinHint JoinHint():
73527352
}
73537353
}
73547354

7355+
JoinHint DorisJoinHint():
7356+
{
7357+
Token token;
7358+
}
7359+
{
7360+
"["
7361+
(
7362+
LOOKAHEAD({ isKeywordAhead("SHUFFLE") }) token=<S_IDENTIFIER>
7363+
|
7364+
LOOKAHEAD({ isKeywordAhead("BROADCAST") }) token=<S_IDENTIFIER>
7365+
)
7366+
"]"
7367+
{ return new JoinHint(token.image, JoinHint.Position.AFTER_JOIN); }
7368+
}
7369+
73557370
Join JoinerExpression() #JoinerExpression:
73567371
{
73577372
Join join = new Join();
@@ -7391,6 +7406,14 @@ Join JoinerExpression() #JoinerExpression:
73917406
(
73927407
[ joinHint=JoinHint() {join.setJoinHint(joinHint); } ]
73937408
<K_JOIN>
7409+
[ LOOKAHEAD("[", { Dialect.DORIS.name().equals(getAsString(Feature.dialect)) })
7410+
{
7411+
if (joinHint != null) {
7412+
throw new ParseException("Only one join hint is allowed per JOIN");
7413+
}
7414+
}
7415+
joinHint=DorisJoinHint() { join.setJoinHint(joinHint); }
7416+
]
73947417
[ <K_FETCH> { join.setFetch(true); } ]
73957418
)
73967419
|

‎src/site/sphinx/usage.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
723723
- Informix ``ALTER TABLE ... ADD CONSTRAINT`` definitions with optional trailing constraint names
724724
* - ``SPANNER``
725725
- GoogleSQL ``CREATE [UNIQUE] NULL_FILTERED INDEX`` with a separate null-filtering flag
726+
* - ``DORIS``
727+
- ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints
726728

727729
Features set explicitly *after* the preset win over it.
728730

@@ -732,6 +734,11 @@ MySQL user-variable targets in ``SELECT ... INTO @variable`` require
732734
expressions, with the clause position preserved before ``FROM`` or at the end
733735
of the query. They are not table targets in ``getIntoTables()``.
734736

737+
Doris distribution hints require ``parser.withDialect(Dialect.DORIS)``.
738+
``Join.getJoinHint()`` exposes the keyword and ``Position.AFTER_JOIN``;
739+
the existing SQL Server hints use ``Position.BEFORE_JOIN``. Rendering preserves
740+
both the position and the brackets around a Doris hint.
741+
735742
With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
736743
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
737744
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNull;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
import java.util.List;
16+
import java.util.Set;
17+
import net.sf.jsqlparser.JSQLParserException;
18+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
19+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
20+
import net.sf.jsqlparser.schema.Table;
21+
import net.sf.jsqlparser.statement.Statement;
22+
import net.sf.jsqlparser.test.TestUtils;
23+
import net.sf.jsqlparser.util.TablesNamesFinder;
24+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.params.ParameterizedTest;
27+
import org.junit.jupiter.params.provider.ValueSource;
28+
29+
class DorisJoinHintTest {
30+
private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException {
31+
return (PlainSelect) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect));
32+
}
33+
34+
@Test
35+
void parsesOriginalReproducerIssue1620() throws Exception {
36+
String sql = "SELECT * FROM uba.events a LEFT JOIN [shuffle] uba.events b "
37+
+ "ON a.event_id = b.event_id";
38+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false,
39+
parser -> parser.withDialect(Dialect.DORIS));
40+
Join join = select.getJoins().get(0);
41+
assertEquals("shuffle", join.getJoinHint().getKeyword());
42+
assertEquals(JoinHint.Position.AFTER_JOIN, join.getJoinHint().getPosition());
43+
assertEquals("uba.events", ((Table) join.getFromItem()).getFullyQualifiedName());
44+
assertEquals("b", join.getFromItem().getAlias().getName());
45+
assertEquals(Set.of("uba.events"), new TablesNamesFinder<>().getTables((Statement) select));
46+
}
47+
48+
@ParameterizedTest
49+
@ValueSource(strings = {"shuffle", "broadcast", "SHUFFLE", "BROADCAST"})
50+
void preservesHintPositionAndRoundTrips(String hint) throws Exception {
51+
for (String kind : List.of("", "INNER ", "LEFT OUTER ", "RIGHT ", "LEFT SEMI ")) {
52+
String sql = "SELECT a.id FROM a " + kind + "JOIN [" + hint + "] b USING (id)";
53+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false,
54+
parser -> parser.withDialect(Dialect.DORIS));
55+
StringBuilder output = new StringBuilder();
56+
select.accept(new StatementDeParser(output), null);
57+
for (String rendered : List.of(select.toString(), output.toString())) {
58+
JoinHint reparsed = parse(rendered, Dialect.DORIS).getJoins().get(0).getJoinHint();
59+
assertEquals(hint, reparsed.getKeyword());
60+
assertEquals(JoinHint.Position.AFTER_JOIN, reparsed.getPosition());
61+
}
62+
}
63+
}
64+
65+
@Test
66+
void keepsHintPerJoinAndSupportsAstMutation() throws Exception {
67+
PlainSelect select = parse("SELECT * FROM a JOIN [shuffle] b ON a.id = b.id "
68+
+ "JOIN [broadcast] c ON b.id = c.id JOIN d ON c.id = d.id", Dialect.DORIS);
69+
assertEquals("shuffle", select.getJoins().get(0).getJoinHint().getKeyword());
70+
assertEquals("broadcast", select.getJoins().get(1).getJoinHint().getKeyword());
71+
assertNull(select.getJoins().get(2).getJoinHint());
72+
select.getJoins().get(0)
73+
.setJoinHint(new JoinHint("broadcast", JoinHint.Position.AFTER_JOIN));
74+
TestUtils.assertDeparse(select, "SELECT * FROM a JOIN [broadcast] b ON a.id = b.id "
75+
+ "JOIN [broadcast] c ON b.id = c.id JOIN d ON c.id = d.id");
76+
select.getJoins().get(0).setJoinHint(null);
77+
assertEquals("JOIN b ON a.id = b.id", select.getJoins().get(0).toString());
78+
}
79+
80+
@Test
81+
void keepsSqlServerHintsAndQuotedTableNames() throws Exception {
82+
for (String keyword : List.of("LOOP", "HASH", "MERGE", "REMOTE")) {
83+
String sql = "SELECT * FROM a INNER " + keyword + " JOIN b ON a.id = b.id";
84+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql);
85+
assertEquals(JoinHint.Position.BEFORE_JOIN,
86+
select.getJoins().get(0).getJoinHint().getPosition());
87+
assertEquals(keyword, new JoinHint(keyword).toString());
88+
}
89+
PlainSelect quoted = parse("SELECT * FROM a JOIN [shuffle] b ON a.id = b.id",
90+
Dialect.SQLSERVER);
91+
assertNull(quoted.getJoins().get(0).getJoinHint());
92+
assertEquals("[shuffle]", ((Table) quoted.getJoins().get(0).getFromItem()).getName());
93+
PlainSelect dorisTable = parse("SELECT * FROM a JOIN `shuffle` b ON a.id = b.id",
94+
Dialect.DORIS);
95+
assertNull(dorisTable.getJoins().get(0).getJoinHint());
96+
}
97+
98+
@Test
99+
void requiresDorisDialect() {
100+
String sql = "SELECT * FROM a JOIN [shuffle] db.b b ON a.id = b.id";
101+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
102+
for (Dialect dialect : Dialect.values()) {
103+
if (dialect != Dialect.DORIS) {
104+
assertThrows(JSQLParserException.class, () -> parse(sql, dialect), dialect.name());
105+
}
106+
}
107+
}
108+
109+
@Test
110+
void rejectsUnknownAndDuplicateHints() {
111+
for (String sql : List.of("SELECT * FROM a JOIN [unknown] b ON a.id = b.id",
112+
"SELECT * FROM a JOIN [shuffle, broadcast] b ON a.id = b.id",
113+
"SELECT * FROM a JOIN [shuffle] [broadcast] b ON a.id = b.id",
114+
"SELECT * FROM a INNER HASH JOIN [shuffle] b ON a.id = b.id",
115+
"SELECT * FROM a JOIN [shuffle b ON a.id = b.id")) {
116+
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.DORIS), sql);
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)