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
15 changes: 15 additions & 0 deletions src/main/java/net/sf/jsqlparser/expression/SQLServerHints.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ public class SQLServerHints implements Serializable {

private Boolean noLock;
private String indexName;
private final List<LockHint> lockHints = new ArrayList<>();

/** Additional argument-free locking hints; NOLOCK retains its existing accessors. */
public enum LockHint {
HOLDLOCK, NOWAIT, PAGLOCK, READCOMMITTED, READCOMMITTEDLOCK, READPAST, READUNCOMMITTED, REPEATABLEREAD, ROWLOCK, SERIALIZABLE, SNAPSHOT, TABLOCK, TABLOCKX, UPDLOCK, XLOCK
}

/** Returns the mutable list of locking hints, in their original order. */
public List<LockHint> getLockHints() {
return lockHints;
}


public SQLServerHints() {}

Expand Down Expand Up @@ -50,6 +62,9 @@ public String toString() {
if (Boolean.TRUE.equals(noLock)) {
hints.add("NOLOCK");
}
for (LockHint hint : lockHints) {
hints.add(hint.name());
}
return " WITH ("
+ String.join(", ", hints)
+ ")";
Expand Down
12 changes: 12 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -6532,10 +6532,12 @@ Table TableWithAlias():
{
Table table = null;
Alias alias = null;
SQLServerHints sqlServerHints;
}
{
table=TableReference()
[ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); }]
[ LOOKAHEAD(<K_WITH> "(") sqlServerHints=SQLServerHints() { table.setSqlServerHints(sqlServerHints); } ]
{ return table; }
}

Expand Down Expand Up @@ -8163,11 +8165,21 @@ List<Alias.AliasColumn> AliasColumns():

void SQLServerHint(SQLServerHints hints) : {
String str;
Token keyword;
}
{
<K_INDEX> "(" str = RelObjectName() ")" { hints.setIndexName(str); }
|
<K_NOLOCK> { hints.withNoLock(); }
|
( keyword=<S_IDENTIFIER> | keyword=<K_NOWAIT> )
{
try {
hints.getLockHints().add(SQLServerHints.LockHint.valueOf(keyword.image.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException exception) {
throw new ParseException("Unsupported SQL Server locking hint: " + keyword.image);
}
}
}

SQLServerHints SQLServerHints() : {
Expand Down
102 changes: 102 additions & 0 deletions src/test/java/net/sf/jsqlparser/expression/SQLServerHintsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*-
* #%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.expression;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.SQLServerHints.LockHint;
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.statement.select.PlainSelect;
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.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;

class SQLServerHintsTest {
@ParameterizedTest
@EnumSource(LockHint.class)
void parsesEachLockingHint(LockHint hint) throws Exception {
Statement statement = parse("SELECT * FROM dbo.jobs WITH (" + hint + ")");
assertEquals(List.of(hint), hints(statement).getLockHints());
roundTrip(statement);
}

@ParameterizedTest
@ValueSource(strings = {"SELECT * FROM dbo.jobs WITH (UPDLOCK, ROWLOCK)",
"SELECT * FROM [dbo].[jobs] AS j WITH (INDEX ([ix_jobs]), UPDLOCK, ROWLOCK) WHERE j.id = 1",
"SELECT * FROM jobs WITH (ROWLOCK, READPAST, UPDLOCK) ORDER BY id",
"SELECT * FROM jobs j WITH (HOLDLOCK) JOIN workers w WITH (ROWLOCK, NOWAIT) ON j.id = w.id",
"WITH q AS (SELECT * FROM jobs WITH (UPDLOCK, ROWLOCK)) SELECT * FROM q",
"UPDATE j SET state = 1 FROM jobs j WITH (UPDLOCK, ROWLOCK) WHERE id = 1",
"DELETE j FROM jobs j WITH (ROWLOCK) WHERE id = 1",
"DELETE FROM jobs WITH (ROWLOCK) WHERE id = 1",
"UPDATE jobs WITH (ROWLOCK, UPDLOCK) SET state = 1 WHERE id = 1",
"SELECT * FROM jobs WITH (NOLOCK)",
"SELECT * FROM jobs WITH (INDEX (ix_jobs), NOLOCK)"})
void retainsHintsAcrossAliasesJoinsCtesAndDml(String sql) throws Exception {
roundTrip(parse(sql));
}

@Test
void exposesTypedHintsWithoutChangingLegacyAccessors() throws Exception {
Statement statement = parse("SELECT * FROM jobs WITH (updlock, rowlock)");
SQLServerHints hints = hints(statement);
assertEquals(List.of(LockHint.UPDLOCK, LockHint.ROWLOCK), hints.getLockHints());
hints.getLockHints().set(0, LockHint.READPAST);
hints.setIndexName("ix_jobs");
assertEquals("SELECT * FROM jobs WITH (INDEX (ix_jobs), READPAST, ROWLOCK)",
statement.toString());
assertThat(new TablesNamesFinder().getTables(statement)).containsExactly("jobs");
roundTrip(statement);

SQLServerHints legacy = new SQLServerHints();
assertNull(legacy.getNoLock());
legacy.withNoLock().withIndexName("ix");
assertEquals(Boolean.TRUE, legacy.getNoLock());
assertEquals(" WITH (INDEX (ix), NOLOCK)", legacy.toString());
legacy.setNoLock(false);
assertEquals(Boolean.FALSE, legacy.getNoLock());
assertEquals(" WITH (INDEX (ix))", legacy.toString());
legacy.withNoLock(null);
assertNull(legacy.getNoLock());
}

@ParameterizedTest
@ValueSource(strings = {"SELECT * FROM jobs WITH ()", "SELECT * FROM jobs WITH (ROWLOCK,)",
"SELECT * FROM jobs WITH (,UPDLOCK)", "SELECT * FROM jobs WITH (UPDLOCK(1))",
"SELECT * FROM jobs WITH (ROWLOCK = 1)", "SELECT * FROM jobs WITH (UNKNOWN_HINT)",
"SELECT * FROM jobs WITH ('ROWLOCK')", "SELECT * FROM jobs WITH (ROWLOCK"})
void rejectsMalformedOrUnknownHints(String sql) {
assertThrows(JSQLParserException.class, () -> parse(sql));
}

private static SQLServerHints hints(Statement statement) {
return ((Table) ((PlainSelect) statement).getFromItem()).getSqlServerHints();
}

private static Statement parse(String sql) throws JSQLParserException {
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER));
}

private static void roundTrip(Statement statement) throws Exception {
StringBuilder output = new StringBuilder();
statement.accept(new StatementDeParser(output), null);
assertEquals(statement.toString(), output.toString());
assertEquals(output.toString(), parse(output.toString()).toString());
}
}
Loading