Skip to content

Commit f925239

Browse files
fix: parse PostgreSQL SELECT with more than one locking clause (#2927)
PostgreSQL accepts several locking clauses in one SELECT, such as FOR UPDATE OF t FOR SHARE OF u. After the first clause, PlainSelect now reads further FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE and FOR KEY SHARE clauses, each with its optional OF list, WAIT, NOWAIT or SKIP LOCKED, into Select.getAdditionalForUpdateClauses(). The first clause keeps its existing getters. toString, the deparser and the validator handle the additional clauses. Fixes #2926 Signed-off-by: Dylan Merigaud <dylanmerigaud@gmail.com>
1 parent 07d5f32 commit f925239

6 files changed

Lines changed: 154 additions & 0 deletions

File tree

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp
4444
private Wait wait;
4545
private boolean noWait = false;
4646
private boolean forUpdateBeforeOrderBy = false;
47+
private List<ForUpdateClause> additionalForUpdateClauses = null;
4748
Alias alias;
4849
Pivot pivot;
4950
UnPivot unPivot;
@@ -416,6 +417,30 @@ public ForUpdateClause getForUpdate() {
416417
return clause;
417418
}
418419

420+
/**
421+
* Returns the locking clauses that follow the first one, as in PostgreSQL's
422+
* {@code FOR UPDATE OF t FOR SHARE OF u}, or {@code null} if there are none. The first clause
423+
* is the one {@link #getForMode()}, {@link #getForUpdateTables()}, {@link #getWait()},
424+
* {@link #isNoWait()} and {@link #isSkipLocked()} describe.
425+
*
426+
* @return the locking clauses after the first one, or {@code null}
427+
*/
428+
public List<ForUpdateClause> getAdditionalForUpdateClauses() {
429+
return additionalForUpdateClauses;
430+
}
431+
432+
public void setAdditionalForUpdateClauses(List<ForUpdateClause> additionalForUpdateClauses) {
433+
this.additionalForUpdateClauses = additionalForUpdateClauses;
434+
}
435+
436+
public Select addAdditionalForUpdateClause(ForUpdateClause forUpdateClause) {
437+
if (additionalForUpdateClauses == null) {
438+
additionalForUpdateClauses = new ArrayList<>();
439+
}
440+
additionalForUpdateClauses.add(forUpdateClause);
441+
return this;
442+
}
443+
419444
/**
420445
* Returns {@code true} when the {@code FOR UPDATE} clause appears before the {@code ORDER BY}
421446
* clause in the original SQL (non-standard ordering supported by some databases).
@@ -572,6 +597,11 @@ public StringBuilder appendTo(StringBuilder builder) {
572597
} else if (isSkipLocked()) {
573598
builder.append(" SKIP LOCKED");
574599
}
600+
if (additionalForUpdateClauses != null) {
601+
for (ForUpdateClause forUpdateClause : additionalForUpdateClauses) {
602+
forUpdateClause.appendTo(builder);
603+
}
604+
}
575605
}
576606

577607
if (forUpdateBeforeOrderBy) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import net.sf.jsqlparser.statement.select.Distinct;
5151
import net.sf.jsqlparser.statement.select.Fetch;
5252
import net.sf.jsqlparser.statement.select.First;
53+
import net.sf.jsqlparser.statement.select.ForUpdateClause;
5354
import net.sf.jsqlparser.statement.select.FromItem;
5455
import net.sf.jsqlparser.statement.select.FromItemVisitor;
5556
import net.sf.jsqlparser.statement.select.InterpolateElement;
@@ -313,6 +314,12 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
313314
} else if (plainSelect.isSkipLocked()) {
314315
builder.append(" SKIP LOCKED");
315316
}
317+
if (plainSelect.getAdditionalForUpdateClauses() != null) {
318+
for (ForUpdateClause forUpdateClause : plainSelect
319+
.getAdditionalForUpdateClauses()) {
320+
forUpdateClause.appendTo(builder);
321+
}
322+
}
316323
}
317324

318325
if (plainSelect.isForUpdateBeforeOrderBy()) {

‎src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import net.sf.jsqlparser.statement.select.ExceptOp;
2424
import net.sf.jsqlparser.statement.select.Fetch;
2525
import net.sf.jsqlparser.statement.select.ForMode;
26+
import net.sf.jsqlparser.statement.select.ForUpdateClause;
2627
import net.sf.jsqlparser.statement.select.FromItemVisitor;
2728
import net.sf.jsqlparser.statement.select.InterpolateElement;
2829
import net.sf.jsqlparser.statement.select.IntersectOp;
@@ -142,6 +143,29 @@ public <S> Void visit(PlainSelect plainSelect, S context) {
142143
validateOptionalFeature(c, plainSelect.getWait(), Feature.selectForUpdateWait);
143144
validateFeature(c, plainSelect.isNoWait(), Feature.selectForUpdateNoWait);
144145
validateFeature(c, plainSelect.isSkipLocked(), Feature.selectForUpdateSkipLocked);
146+
147+
if (plainSelect.getAdditionalForUpdateClauses() != null) {
148+
for (ForUpdateClause forUpdateClause : plainSelect
149+
.getAdditionalForUpdateClauses()) {
150+
validateFeature(c, forUpdateClause.getMode() == ForMode.KEY_SHARE,
151+
Feature.selectForKeyShare);
152+
validateFeature(c, forUpdateClause.getMode() == ForMode.NO_KEY_UPDATE,
153+
Feature.selectForNoKeyUpdate);
154+
validateFeature(c, forUpdateClause.getMode() == ForMode.SHARE,
155+
Feature.selectForShare);
156+
if (forUpdateClause.getTables() != null) {
157+
forUpdateClause.getTables()
158+
.forEach(t -> validateOptionalFeature(c, t,
159+
Feature.selectForUpdateOfTable));
160+
}
161+
validateOptionalFeature(c, forUpdateClause.getWait(),
162+
Feature.selectForUpdateWait);
163+
validateFeature(c, forUpdateClause.isNoWait(),
164+
Feature.selectForUpdateNoWait);
165+
validateFeature(c, forUpdateClause.isSkipLocked(),
166+
Feature.selectForUpdateSkipLocked);
167+
}
168+
}
145169
}
146170

147171
validateOptionalFeature(c, plainSelect.getForXmlPath(), Feature.selectForXmlPath);

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12943,6 +12943,34 @@ void MySqlSelectModifier(PlainSelect select):
1294312943
}
1294412944
}
1294512945

12946+
ForUpdateClause AdditionalForUpdateClause():
12947+
{
12948+
ForUpdateClause clause = new ForUpdateClause();
12949+
List<Table> tables = new ArrayList<Table>();
12950+
Table table;
12951+
Wait wait;
12952+
}
12953+
{
12954+
<K_FOR>
12955+
(
12956+
<K_UPDATE> { clause.setMode(ForMode.UPDATE); }
12957+
| <K_SHARE> { clause.setMode(ForMode.SHARE); }
12958+
| (<K_NO> <K_KEY> <K_UPDATE> { clause.setMode(ForMode.NO_KEY_UPDATE); })
12959+
| (<K_KEY> <K_SHARE> { clause.setMode(ForMode.KEY_SHARE); })
12960+
)
12961+
[ LOOKAHEAD(2) <K_OF>
12962+
table = Table() { tables.add(table); }
12963+
( LOOKAHEAD(2) "," table = Table() { tables.add(table); } )*
12964+
{ clause.setTables(tables); }
12965+
]
12966+
[ LOOKAHEAD(1, <K_WAIT>) wait = Wait() { clause.setWait(wait); } ]
12967+
[ LOOKAHEAD(2) (<K_NOWAIT> { clause.setNoWait(true); }
12968+
| <K_SKIP> <K_LOCKED> { clause.setSkipLocked(true); }) ]
12969+
{
12970+
return clause;
12971+
}
12972+
}
12973+
1294612974
ForClause ForClause() #ForClause:
1294712975
{
1294812976
Token token = null;
@@ -13079,6 +13107,7 @@ PlainSelect PlainSelect() #PlainSelect:
1307913107
MySqlProcedureAnalyse mySqlProcedureAnalyse = null;
1308013108
Table updateTable = null;
1308113109
List<Table> updateTables = new ArrayList<Table>();
13110+
ForUpdateClause additionalForUpdateClause;
1308213111
Wait wait = null;
1308313112
Token token;
1308413113
KSQLWindow ksqlWindow = null;
@@ -13245,6 +13274,11 @@ PlainSelect PlainSelect() #PlainSelect:
1324513274
[ LOOKAHEAD(1, <K_WAIT>) wait = Wait() { plainSelect.setWait(wait); } ]
1324613275
[ LOOKAHEAD(2) (<K_NOWAIT> { plainSelect.setNoWait(true); }
1324713276
| <K_SKIP> <K_LOCKED> { plainSelect.setSkipLocked(true); }) ]
13277+
// PostgreSQL: more locking clauses, e.g. FOR UPDATE OF t FOR SHARE OF u
13278+
( LOOKAHEAD(<K_FOR> (<K_UPDATE> | <K_SHARE> | <K_NO> <K_KEY> | <K_KEY> <K_SHARE>))
13279+
additionalForUpdateClause = AdditionalForUpdateClause()
13280+
{ plainSelect.addAdditionalForUpdateClause(additionalForUpdateClause); }
13281+
)*
1324813282
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements()
1324913283
{ plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); }
1325013284
]

‎src/test/java/net/sf/jsqlparser/statement/select/ForUpdateTest.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@
1111

1212
import net.sf.jsqlparser.JSQLParserException;
1313
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
14+
import net.sf.jsqlparser.schema.Table;
1415
import net.sf.jsqlparser.statement.Statement;
1516
import net.sf.jsqlparser.test.TestUtils;
1617
import org.junit.jupiter.api.Test;
1718

19+
import java.util.List;
20+
1821
import static org.assertj.core.api.Assertions.assertThat;
1922

2023
public class ForUpdateTest {
@@ -145,4 +148,50 @@ void testForUpdateWait() throws JSQLParserException {
145148
assertThat(forUpdate.getWait()).isNotNull();
146149
assertThat(forUpdate.getWait().getTimeout()).isEqualTo(10L);
147150
}
151+
152+
@Test
153+
void testPostgresMultipleLockingClausesIssue2926() throws JSQLParserException {
154+
String sqlStr = "SELECT * FROM t JOIN u ON u.t_id = t.id FOR UPDATE OF t FOR SHARE OF u";
155+
Statement stmt = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
156+
PlainSelect plainSelect = (PlainSelect) stmt;
157+
158+
assertThat(plainSelect.getForMode()).isEqualTo(ForMode.UPDATE);
159+
assertThat(plainSelect.getForUpdateTables()).extracting(Table::getName)
160+
.containsExactly("t");
161+
162+
assertThat(plainSelect.getAdditionalForUpdateClauses()).hasSize(1);
163+
ForUpdateClause second = plainSelect.getAdditionalForUpdateClauses().get(0);
164+
assertThat(second.isForShare()).isTrue();
165+
assertThat(second.getTables()).extracting(Table::getName).containsExactly("u");
166+
}
167+
168+
@Test
169+
void testPostgresMultipleLockingClausesWithOptions() throws JSQLParserException {
170+
String sqlStr = "SELECT * FROM a, b, c, d"
171+
+ " FOR NO KEY UPDATE OF a NOWAIT"
172+
+ " FOR KEY SHARE OF b, c SKIP LOCKED"
173+
+ " FOR SHARE"
174+
+ " FOR UPDATE OF d NOWAIT";
175+
Statement stmt = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true);
176+
PlainSelect plainSelect = (PlainSelect) stmt;
177+
178+
assertThat(plainSelect.getForMode()).isEqualTo(ForMode.NO_KEY_UPDATE);
179+
assertThat(plainSelect.isNoWait()).isTrue();
180+
181+
List<ForUpdateClause> more = plainSelect.getAdditionalForUpdateClauses();
182+
assertThat(more).extracting(ForUpdateClause::getMode)
183+
.containsExactly(ForMode.KEY_SHARE, ForMode.SHARE, ForMode.UPDATE);
184+
assertThat(more.get(0).getTables()).extracting(Table::getName)
185+
.containsExactly("b", "c");
186+
assertThat(more.get(0).isSkipLocked()).isTrue();
187+
assertThat(more.get(1).getTables()).isNull();
188+
assertThat(more.get(2).isNoWait()).isTrue();
189+
}
190+
191+
@Test
192+
void testSingleLockingClauseHasNoAdditionalClauses() throws JSQLParserException {
193+
PlainSelect plainSelect =
194+
(PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM t FOR UPDATE OF t");
195+
assertThat(plainSelect.getAdditionalForUpdateClauses()).isNull();
196+
}
148197
}

‎src/test/java/net/sf/jsqlparser/util/validation/validator/SelectValidatorTest.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ public void testValidationForPostgresShare() throws JSQLParserException {
121121
Feature.selectForNoKeyUpdate);
122122
}
123123

124+
@Test
125+
public void testValidationMultipleLockingClauses() throws JSQLParserException {
126+
String sql = "SELECT * FROM t, u FOR UPDATE OF t FOR SHARE OF u";
127+
validateNoErrors(sql, 1, DatabaseType.POSTGRESQL);
128+
129+
String sql2 = "SELECT * FROM t FOR UPDATE FOR NO KEY UPDATE";
130+
validateNotSupported(sql2, 1, 1, DatabaseType.ORACLE,
131+
Feature.selectForNoKeyUpdate);
132+
}
133+
124134
@Test
125135
public void testValidationForUpdateNoWait() throws JSQLParserException {
126136
String sql = "SELECT * FROM mytable FOR UPDATE NOWAIT";

0 commit comments

Comments
 (0)