Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public enum Dialect {
AdjacentStringLiterals.WHITESPACE,
Feature.allowDoubleQuotedStrings,
Feature.allowBackslashEscapeCharacter), SNOWFLAKE(
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER;
Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS;

private final Set<Feature> lexerFeatures;
private final AdjacentStringLiterals adjacentStringLiterals;
Expand Down
35 changes: 22 additions & 13 deletions src/main/java/net/sf/jsqlparser/statement/select/Join.java
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,27 @@ public Join setJoinHint(JoinHint joinHint) {
return this;
}

/** Appends the join keyword, hint and FETCH modifier, followed by a space. */
public StringBuilder appendJoinKeywordTo(StringBuilder builder) {
if (isStraight()) {
builder.append("STRAIGHT_JOIN ");
} else if (isApply()) {
builder.append("APPLY ");
} else {
if (joinHint != null && joinHint.getPosition() == JoinHint.Position.BEFORE_JOIN) {
builder.append(joinHint).append(' ');
}
builder.append("JOIN ");
if (joinHint != null && joinHint.getPosition() == JoinHint.Position.AFTER_JOIN) {
builder.append(joinHint).append(' ');
}
if (fetch) {
builder.append("FETCH ");
}
}
return builder;
}

@Override
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
public String toString() {
Expand Down Expand Up @@ -510,19 +531,7 @@ public String toString() {
builder.append("ARRAY ");
}

if (isStraight()) {
builder.append("STRAIGHT_JOIN ");
} else if (isApply()) {
builder.append("APPLY ");
} else {
if (joinHint != null) {
builder.append(joinHint).append(" ");
}
builder.append("JOIN ");
if (fetch) {
builder.append("FETCH ");
}
}
appendJoinKeywordTo(builder);

builder.append(fromItem).append((joinWindow != null) ? " WITHIN " + joinWindow : "");
}
Expand Down
24 changes: 22 additions & 2 deletions src/main/java/net/sf/jsqlparser/statement/select/JoinHint.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,43 @@
*/
package net.sf.jsqlparser.statement.select;

import java.util.Objects;

/**
* Hints (Transact-SQL) - Join
* SQL Server join hints precede JOIN; Doris distribution hints follow it in square brackets.
*
* @link <a href=
* "https://learn.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-join?view=sql-server-ver16">Hints
* (Transact-SQL) - Join</a>
*/

public class JoinHint {
public enum Position {
BEFORE_JOIN, AFTER_JOIN
}

private final String keyword;
private final Position position;

public JoinHint(String keyword) {
this(keyword, Position.BEFORE_JOIN);
}

public JoinHint(String keyword, Position position) {
this.keyword = keyword;
this.position = Objects.requireNonNull(position, "position");
}

public String getKeyword() {
return keyword;
}

public Position getPosition() {
return position;
}

@Override
public String toString() {
return keyword;
return position == Position.AFTER_JOIN ? "[" + keyword + "]" : keyword;
}
}
15 changes: 2 additions & 13 deletions src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -765,19 +765,8 @@ public void deparseJoin(Join join) {
builder.append(" ARRAY");
}

if (join.isStraight()) {
builder.append(" STRAIGHT_JOIN ");
} else if (join.isApply()) {
builder.append(" APPLY ");
} else {
if (join.getJoinHint() != null) {
builder.append(" ").append(join.getJoinHint());
}
builder.append(" JOIN ");
if (join.isFetch()) {
builder.append("FETCH ");
}
}
builder.append(' ');
join.appendJoinKeywordTo(builder);

}

Expand Down
23 changes: 23 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -7352,6 +7352,21 @@ JoinHint JoinHint():
}
}

JoinHint DorisJoinHint():
{
Token token;
}
{
"["
(
LOOKAHEAD({ isKeywordAhead("SHUFFLE") }) token=<S_IDENTIFIER>
|
LOOKAHEAD({ isKeywordAhead("BROADCAST") }) token=<S_IDENTIFIER>
)
"]"
{ return new JoinHint(token.image, JoinHint.Position.AFTER_JOIN); }
}

Join JoinerExpression() #JoinerExpression:
{
Join join = new Join();
Expand Down Expand Up @@ -7391,6 +7406,14 @@ Join JoinerExpression() #JoinerExpression:
(
[ joinHint=JoinHint() {join.setJoinHint(joinHint); } ]
<K_JOIN>
[ LOOKAHEAD("[", { Dialect.DORIS.name().equals(getAsString(Feature.dialect)) })
{
if (joinHint != null) {
throw new ParseException("Only one join hint is allowed per JOIN");
}
}
joinHint=DorisJoinHint() { join.setJoinHint(joinHint); }
]
[ <K_FETCH> { join.setFetch(true); } ]
)
|
Expand Down
7 changes: 7 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,8 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
- Informix ``ALTER TABLE ... ADD CONSTRAINT`` definitions with optional trailing constraint names
* - ``SPANNER``
- GoogleSQL ``CREATE [UNIQUE] NULL_FILTERED INDEX`` with a separate null-filtering flag
* - ``DORIS``
- ``JOIN [shuffle]`` and ``JOIN [broadcast]`` distribution hints

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

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

Doris distribution hints require ``parser.withDialect(Dialect.DORIS)``.
``Join.getJoinHint()`` exposes the keyword and ``Position.AFTER_JOIN``;
the existing SQL Server hints use ``Position.BEFORE_JOIN``. Rendering preserves
both the position and the brackets around a Doris hint.

With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.select;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import java.util.Set;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.test.TestUtils;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class DorisJoinHintTest {
private static PlainSelect parse(String sql, Dialect dialect) throws JSQLParserException {
return (PlainSelect) CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect));
}

@Test
void parsesOriginalReproducerIssue1620() throws Exception {
String sql = "SELECT * FROM uba.events a LEFT JOIN [shuffle] uba.events b "
+ "ON a.event_id = b.event_id";
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false,
parser -> parser.withDialect(Dialect.DORIS));
Join join = select.getJoins().get(0);
assertEquals("shuffle", join.getJoinHint().getKeyword());
assertEquals(JoinHint.Position.AFTER_JOIN, join.getJoinHint().getPosition());
assertEquals("uba.events", ((Table) join.getFromItem()).getFullyQualifiedName());
assertEquals("b", join.getFromItem().getAlias().getName());
assertEquals(Set.of("uba.events"), new TablesNamesFinder<>().getTables((Statement) select));
}

@ParameterizedTest
@ValueSource(strings = {"shuffle", "broadcast", "SHUFFLE", "BROADCAST"})
void preservesHintPositionAndRoundTrips(String hint) throws Exception {
for (String kind : List.of("", "INNER ", "LEFT OUTER ", "RIGHT ", "LEFT SEMI ")) {
String sql = "SELECT a.id FROM a " + kind + "JOIN [" + hint + "] b USING (id)";
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, false,
parser -> parser.withDialect(Dialect.DORIS));
StringBuilder output = new StringBuilder();
select.accept(new StatementDeParser(output), null);
for (String rendered : List.of(select.toString(), output.toString())) {
JoinHint reparsed = parse(rendered, Dialect.DORIS).getJoins().get(0).getJoinHint();
assertEquals(hint, reparsed.getKeyword());
assertEquals(JoinHint.Position.AFTER_JOIN, reparsed.getPosition());
}
}
}

@Test
void keepsHintPerJoinAndSupportsAstMutation() throws Exception {
PlainSelect select = parse("SELECT * FROM a JOIN [shuffle] b ON a.id = b.id "
+ "JOIN [broadcast] c ON b.id = c.id JOIN d ON c.id = d.id", Dialect.DORIS);
assertEquals("shuffle", select.getJoins().get(0).getJoinHint().getKeyword());
assertEquals("broadcast", select.getJoins().get(1).getJoinHint().getKeyword());
assertNull(select.getJoins().get(2).getJoinHint());
select.getJoins().get(0)
.setJoinHint(new JoinHint("broadcast", JoinHint.Position.AFTER_JOIN));
TestUtils.assertDeparse(select, "SELECT * FROM a JOIN [broadcast] b ON a.id = b.id "
+ "JOIN [broadcast] c ON b.id = c.id JOIN d ON c.id = d.id");
select.getJoins().get(0).setJoinHint(null);
assertEquals("JOIN b ON a.id = b.id", select.getJoins().get(0).toString());
}

@Test
void keepsSqlServerHintsAndQuotedTableNames() throws Exception {
for (String keyword : List.of("LOOP", "HASH", "MERGE", "REMOTE")) {
String sql = "SELECT * FROM a INNER " + keyword + " JOIN b ON a.id = b.id";
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql);
assertEquals(JoinHint.Position.BEFORE_JOIN,
select.getJoins().get(0).getJoinHint().getPosition());
assertEquals(keyword, new JoinHint(keyword).toString());
}
PlainSelect quoted = parse("SELECT * FROM a JOIN [shuffle] b ON a.id = b.id",
Dialect.SQLSERVER);
assertNull(quoted.getJoins().get(0).getJoinHint());
assertEquals("[shuffle]", ((Table) quoted.getJoins().get(0).getFromItem()).getName());
PlainSelect dorisTable = parse("SELECT * FROM a JOIN `shuffle` b ON a.id = b.id",
Dialect.DORIS);
assertNull(dorisTable.getJoins().get(0).getJoinHint());
}

@Test
void requiresDorisDialect() {
String sql = "SELECT * FROM a JOIN [shuffle] db.b b ON a.id = b.id";
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
for (Dialect dialect : Dialect.values()) {
if (dialect != Dialect.DORIS) {
assertThrows(JSQLParserException.class, () -> parse(sql, dialect), dialect.name());
}
}
}

@Test
void rejectsUnknownAndDuplicateHints() {
for (String sql : List.of("SELECT * FROM a JOIN [unknown] b ON a.id = b.id",
"SELECT * FROM a JOIN [shuffle, broadcast] b ON a.id = b.id",
"SELECT * FROM a JOIN [shuffle] [broadcast] b ON a.id = b.id",
"SELECT * FROM a INNER HASH JOIN [shuffle] b ON a.id = b.id",
"SELECT * FROM a JOIN [shuffle b ON a.id = b.id")) {
assertThrows(JSQLParserException.class, () -> parse(sql, Dialect.DORIS), sql);
}
}
}
Loading