Skip to content

Commit 60c8dc2

Browse files
authored
feat: support SQL Server table locking hints (#2673)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent fafeb79 commit 60c8dc2

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/SQLServerHints.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ public class SQLServerHints implements Serializable {
1717

1818
private Boolean noLock;
1919
private String indexName;
20+
private final List<LockHint> lockHints = new ArrayList<>();
21+
22+
/** Additional argument-free locking hints; NOLOCK retains its existing accessors. */
23+
public enum LockHint {
24+
HOLDLOCK, NOWAIT, PAGLOCK, READCOMMITTED, READCOMMITTEDLOCK, READPAST, READUNCOMMITTED, REPEATABLEREAD, ROWLOCK, SERIALIZABLE, SNAPSHOT, TABLOCK, TABLOCKX, UPDLOCK, XLOCK
25+
}
26+
27+
/** Returns the mutable list of locking hints, in their original order. */
28+
public List<LockHint> getLockHints() {
29+
return lockHints;
30+
}
31+
2032

2133
public SQLServerHints() {}
2234

@@ -50,6 +62,9 @@ public String toString() {
5062
if (Boolean.TRUE.equals(noLock)) {
5163
hints.add("NOLOCK");
5264
}
65+
for (LockHint hint : lockHints) {
66+
hints.add(hint.name());
67+
}
5368
return " WITH ("
5469
+ String.join(", ", hints)
5570
+ ")";

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6559,10 +6559,12 @@ Table TableWithAlias():
65596559
{
65606560
Table table = null;
65616561
Alias alias = null;
6562+
SQLServerHints sqlServerHints;
65626563
}
65636564
{
65646565
table=TableReference()
65656566
[ LOOKAHEAD({ isAliasAhead() }) alias=Alias() { table.setAlias(alias); }]
6567+
[ LOOKAHEAD(<K_WITH> "(") sqlServerHints=SQLServerHints() { table.setSqlServerHints(sqlServerHints); } ]
65666568
{ return table; }
65676569
}
65686570

@@ -8190,11 +8192,21 @@ List<Alias.AliasColumn> AliasColumns():
81908192

81918193
void SQLServerHint(SQLServerHints hints) : {
81928194
String str;
8195+
Token keyword;
81938196
}
81948197
{
81958198
<K_INDEX> "(" str = RelObjectName() ")" { hints.setIndexName(str); }
81968199
|
81978200
<K_NOLOCK> { hints.withNoLock(); }
8201+
|
8202+
( keyword=<S_IDENTIFIER> | keyword=<K_NOWAIT> )
8203+
{
8204+
try {
8205+
hints.getLockHints().add(SQLServerHints.LockHint.valueOf(keyword.image.toUpperCase(Locale.ROOT)));
8206+
} catch (IllegalArgumentException exception) {
8207+
throw new ParseException("Unsupported SQL Server locking hint: " + keyword.image);
8208+
}
8209+
}
81988210
}
81998211

82008212
SQLServerHints SQLServerHints() : {
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.expression;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
import java.util.List;
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.SQLServerHints.LockHint;
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.statement.select.PlainSelect;
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.EnumSource;
28+
import org.junit.jupiter.params.provider.ValueSource;
29+
30+
class SQLServerHintsTest {
31+
@ParameterizedTest
32+
@EnumSource(LockHint.class)
33+
void parsesEachLockingHint(LockHint hint) throws Exception {
34+
Statement statement = parse("SELECT * FROM dbo.jobs WITH (" + hint + ")");
35+
assertEquals(List.of(hint), hints(statement).getLockHints());
36+
roundTrip(statement);
37+
}
38+
39+
@ParameterizedTest
40+
@ValueSource(strings = {"SELECT * FROM dbo.jobs WITH (UPDLOCK, ROWLOCK)",
41+
"SELECT * FROM [dbo].[jobs] AS j WITH (INDEX ([ix_jobs]), UPDLOCK, ROWLOCK) WHERE j.id = 1",
42+
"SELECT * FROM jobs WITH (ROWLOCK, READPAST, UPDLOCK) ORDER BY id",
43+
"SELECT * FROM jobs j WITH (HOLDLOCK) JOIN workers w WITH (ROWLOCK, NOWAIT) ON j.id = w.id",
44+
"WITH q AS (SELECT * FROM jobs WITH (UPDLOCK, ROWLOCK)) SELECT * FROM q",
45+
"UPDATE j SET state = 1 FROM jobs j WITH (UPDLOCK, ROWLOCK) WHERE id = 1",
46+
"DELETE j FROM jobs j WITH (ROWLOCK) WHERE id = 1",
47+
"DELETE FROM jobs WITH (ROWLOCK) WHERE id = 1",
48+
"UPDATE jobs WITH (ROWLOCK, UPDLOCK) SET state = 1 WHERE id = 1",
49+
"SELECT * FROM jobs WITH (NOLOCK)",
50+
"SELECT * FROM jobs WITH (INDEX (ix_jobs), NOLOCK)"})
51+
void retainsHintsAcrossAliasesJoinsCtesAndDml(String sql) throws Exception {
52+
roundTrip(parse(sql));
53+
}
54+
55+
@Test
56+
void exposesTypedHintsWithoutChangingLegacyAccessors() throws Exception {
57+
Statement statement = parse("SELECT * FROM jobs WITH (updlock, rowlock)");
58+
SQLServerHints hints = hints(statement);
59+
assertEquals(List.of(LockHint.UPDLOCK, LockHint.ROWLOCK), hints.getLockHints());
60+
hints.getLockHints().set(0, LockHint.READPAST);
61+
hints.setIndexName("ix_jobs");
62+
assertEquals("SELECT * FROM jobs WITH (INDEX (ix_jobs), READPAST, ROWLOCK)",
63+
statement.toString());
64+
assertThat(new TablesNamesFinder().getTables(statement)).containsExactly("jobs");
65+
roundTrip(statement);
66+
67+
SQLServerHints legacy = new SQLServerHints();
68+
assertNull(legacy.getNoLock());
69+
legacy.withNoLock().withIndexName("ix");
70+
assertEquals(Boolean.TRUE, legacy.getNoLock());
71+
assertEquals(" WITH (INDEX (ix), NOLOCK)", legacy.toString());
72+
legacy.setNoLock(false);
73+
assertEquals(Boolean.FALSE, legacy.getNoLock());
74+
assertEquals(" WITH (INDEX (ix))", legacy.toString());
75+
legacy.withNoLock(null);
76+
assertNull(legacy.getNoLock());
77+
}
78+
79+
@ParameterizedTest
80+
@ValueSource(strings = {"SELECT * FROM jobs WITH ()", "SELECT * FROM jobs WITH (ROWLOCK,)",
81+
"SELECT * FROM jobs WITH (,UPDLOCK)", "SELECT * FROM jobs WITH (UPDLOCK(1))",
82+
"SELECT * FROM jobs WITH (ROWLOCK = 1)", "SELECT * FROM jobs WITH (UNKNOWN_HINT)",
83+
"SELECT * FROM jobs WITH ('ROWLOCK')", "SELECT * FROM jobs WITH (ROWLOCK"})
84+
void rejectsMalformedOrUnknownHints(String sql) {
85+
assertThrows(JSQLParserException.class, () -> parse(sql));
86+
}
87+
88+
private static SQLServerHints hints(Statement statement) {
89+
return ((Table) ((PlainSelect) statement).getFromItem()).getSqlServerHints();
90+
}
91+
92+
private static Statement parse(String sql) throws JSQLParserException {
93+
return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.SQLSERVER));
94+
}
95+
96+
private static void roundTrip(Statement statement) throws Exception {
97+
StringBuilder output = new StringBuilder();
98+
statement.accept(new StatementDeParser(output), null);
99+
assertEquals(statement.toString(), output.toString());
100+
assertEquals(output.toString(), parse(output.toString()).toString());
101+
}
102+
}

0 commit comments

Comments
 (0)