Skip to content

Commit 2b14156

Browse files
rrobettiCopilot
andauthored
feat: add ForUpdateClause class with multi-table and ORDER BY support (#2426)
Agent-Logs-Url: https://github.com/rrobetti/JSqlParser/sessions/1a91a42d-ed37-490e-88b9-25c45e621b64 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rrobetti <7221783+rrobetti@users.noreply.github.com>
1 parent cf5bbc9 commit 2b14156

File tree

9 files changed

+370
-12
lines changed

9 files changed

+370
-12
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2024 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 java.util.List;
13+
import net.sf.jsqlparser.schema.Table;
14+
15+
/**
16+
* Represents a FOR UPDATE / FOR SHARE locking clause in a SELECT statement.
17+
*
18+
* <p>
19+
* Supports all common SQL dialects:
20+
* <ul>
21+
* <li>{@code FOR UPDATE} – standard row locking</li>
22+
* <li>{@code FOR UPDATE OF t1, t2} – table-specific locking (Oracle, PostgreSQL)</li>
23+
* <li>{@code FOR UPDATE NOWAIT} – fail immediately if rows are locked (Oracle, PostgreSQL)</li>
24+
* <li>{@code FOR UPDATE WAIT n} – wait up to n seconds (Oracle)</li>
25+
* <li>{@code FOR UPDATE SKIP LOCKED} – skip locked rows (Oracle, PostgreSQL)</li>
26+
* <li>{@code FOR SHARE} – shared row lock (PostgreSQL)</li>
27+
* <li>{@code FOR KEY SHARE} – key-level shared lock (PostgreSQL)</li>
28+
* <li>{@code FOR NO KEY UPDATE} – non-key exclusive lock (PostgreSQL)</li>
29+
* </ul>
30+
* </p>
31+
*/
32+
public class ForUpdateClause {
33+
34+
private ForMode mode;
35+
private List<Table> tables;
36+
private Wait wait;
37+
private boolean noWait;
38+
private boolean skipLocked;
39+
40+
public ForMode getMode() {
41+
return mode;
42+
}
43+
44+
public ForUpdateClause setMode(ForMode mode) {
45+
this.mode = mode;
46+
return this;
47+
}
48+
49+
public List<Table> getTables() {
50+
return tables;
51+
}
52+
53+
public ForUpdateClause setTables(List<Table> tables) {
54+
this.tables = tables;
55+
return this;
56+
}
57+
58+
/**
59+
* Returns the first table from the OF clause, or {@code null} if none was specified.
60+
*
61+
* @return the first table, or {@code null}
62+
*/
63+
public Table getFirstTable() {
64+
return (tables != null && !tables.isEmpty()) ? tables.get(0) : null;
65+
}
66+
67+
public Wait getWait() {
68+
return wait;
69+
}
70+
71+
public ForUpdateClause setWait(Wait wait) {
72+
this.wait = wait;
73+
return this;
74+
}
75+
76+
public boolean isNoWait() {
77+
return noWait;
78+
}
79+
80+
public ForUpdateClause setNoWait(boolean noWait) {
81+
this.noWait = noWait;
82+
return this;
83+
}
84+
85+
public boolean isSkipLocked() {
86+
return skipLocked;
87+
}
88+
89+
public ForUpdateClause setSkipLocked(boolean skipLocked) {
90+
this.skipLocked = skipLocked;
91+
return this;
92+
}
93+
94+
/** Returns {@code true} when the mode is {@link ForMode#UPDATE}. */
95+
public boolean isForUpdate() {
96+
return mode == ForMode.UPDATE;
97+
}
98+
99+
/** Returns {@code true} when the mode is {@link ForMode#SHARE}. */
100+
public boolean isForShare() {
101+
return mode == ForMode.SHARE;
102+
}
103+
104+
/** Returns {@code true} when at least one table was listed in the OF clause. */
105+
public boolean hasTableList() {
106+
return tables != null && !tables.isEmpty();
107+
}
108+
109+
public StringBuilder appendTo(StringBuilder builder) {
110+
builder.append(" FOR ").append(mode.getValue());
111+
if (tables != null && !tables.isEmpty()) {
112+
builder.append(" OF ");
113+
for (int i = 0; i < tables.size(); i++) {
114+
if (i > 0) {
115+
builder.append(", ");
116+
}
117+
builder.append(tables.get(i));
118+
}
119+
}
120+
if (wait != null) {
121+
builder.append(wait);
122+
}
123+
if (noWait) {
124+
builder.append(" NOWAIT");
125+
} else if (skipLocked) {
126+
builder.append(" SKIP LOCKED");
127+
}
128+
return builder;
129+
}
130+
131+
@Override
132+
public String toString() {
133+
return appendTo(new StringBuilder()).toString();
134+
}
135+
}

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

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import net.sf.jsqlparser.statement.StatementVisitor;
2525

2626
public abstract class Select extends ASTNodeAccessImpl implements Statement, Expression, FromItem {
27-
protected Table forUpdateTable = null;
27+
protected List<Table> forUpdateTables = null;
2828
protected List<WithItem<?>> withItemsList;
2929
Limit limitBy;
3030
Limit limit;
@@ -40,6 +40,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp
4040
private boolean skipLocked;
4141
private Wait wait;
4242
private boolean noWait = false;
43+
private boolean forUpdateBeforeOrderBy = false;
4344
Alias alias;
4445
Pivot pivot;
4546
UnPivot unPivot;
@@ -291,12 +292,92 @@ public void setForMode(ForMode forMode) {
291292
this.forMode = forMode;
292293
}
293294

295+
/**
296+
* Returns the first table from the {@code FOR UPDATE OF} clause, or {@code null} if no table
297+
* was specified. Use {@link #getForUpdateTables()} to retrieve all tables.
298+
*
299+
* @return the first table, or {@code null}
300+
*/
294301
public Table getForUpdateTable() {
295-
return this.forUpdateTable;
302+
return (forUpdateTables != null && !forUpdateTables.isEmpty()) ? forUpdateTables.get(0)
303+
: null;
296304
}
297305

306+
/**
307+
* Sets a single table for the {@code FOR UPDATE OF} clause.
308+
*
309+
* @param forUpdateTable the table, or {@code null} to clear
310+
*/
298311
public void setForUpdateTable(Table forUpdateTable) {
299-
this.forUpdateTable = forUpdateTable;
312+
if (forUpdateTable == null) {
313+
this.forUpdateTables = null;
314+
} else {
315+
this.forUpdateTables = new ArrayList<>();
316+
this.forUpdateTables.add(forUpdateTable);
317+
}
318+
}
319+
320+
/**
321+
* Returns the list of tables named in the {@code FOR UPDATE OF t1, t2, ...} clause, or
322+
* {@code null} if no OF clause was present.
323+
*
324+
* @return list of tables, or {@code null}
325+
*/
326+
public List<Table> getForUpdateTables() {
327+
return forUpdateTables;
328+
}
329+
330+
/**
331+
* Sets the list of tables for the {@code FOR UPDATE OF t1, t2, ...} clause.
332+
*
333+
* @param forUpdateTables list of tables
334+
*/
335+
public void setForUpdateTables(List<Table> forUpdateTables) {
336+
this.forUpdateTables = forUpdateTables;
337+
}
338+
339+
public Select withForUpdateTables(List<Table> forUpdateTables) {
340+
this.setForUpdateTables(forUpdateTables);
341+
return this;
342+
}
343+
344+
/**
345+
* Builds and returns a {@link ForUpdateClause} representing the current FOR UPDATE / FOR SHARE
346+
* state of this SELECT, or {@code null} if no FOR clause is present.
347+
*
348+
* @return a {@link ForUpdateClause} view, or {@code null}
349+
*/
350+
public ForUpdateClause getForUpdate() {
351+
if (forMode == null) {
352+
return null;
353+
}
354+
ForUpdateClause clause = new ForUpdateClause();
355+
clause.setMode(forMode);
356+
clause.setTables(forUpdateTables);
357+
clause.setWait(wait);
358+
clause.setNoWait(noWait);
359+
clause.setSkipLocked(skipLocked);
360+
return clause;
361+
}
362+
363+
/**
364+
* Returns {@code true} when the {@code FOR UPDATE} clause appears before the {@code ORDER BY}
365+
* clause in the original SQL (non-standard ordering supported by some databases).
366+
*
367+
* @return {@code true} if FOR UPDATE precedes ORDER BY
368+
*/
369+
public boolean isForUpdateBeforeOrderBy() {
370+
return forUpdateBeforeOrderBy;
371+
}
372+
373+
/**
374+
* Indicates whether the {@code FOR UPDATE} clause precedes the {@code ORDER BY} clause in the
375+
* SQL output.
376+
*
377+
* @param forUpdateBeforeOrderBy {@code true} to emit FOR UPDATE before ORDER BY
378+
*/
379+
public void setForUpdateBeforeOrderBy(boolean forUpdateBeforeOrderBy) {
380+
this.forUpdateBeforeOrderBy = forUpdateBeforeOrderBy;
300381
}
301382

302383
/**
@@ -380,7 +461,9 @@ public StringBuilder appendTo(StringBuilder builder) {
380461

381462
appendTo(builder, alias, null, pivot, unPivot);
382463

383-
builder.append(orderByToString(oracleSiblings, orderByElements));
464+
if (!forUpdateBeforeOrderBy) {
465+
builder.append(orderByToString(oracleSiblings, orderByElements));
466+
}
384467

385468
if (forClause != null) {
386469
forClause.appendTo(builder);
@@ -405,8 +488,14 @@ public StringBuilder appendTo(StringBuilder builder) {
405488
builder.append(" FOR ");
406489
builder.append(forMode.getValue());
407490

408-
if (getForUpdateTable() != null) {
409-
builder.append(" OF ").append(forUpdateTable);
491+
if (forUpdateTables != null && !forUpdateTables.isEmpty()) {
492+
builder.append(" OF ");
493+
for (int i = 0; i < forUpdateTables.size(); i++) {
494+
if (i > 0) {
495+
builder.append(", ");
496+
}
497+
builder.append(forUpdateTables.get(i));
498+
}
410499
}
411500

412501
if (wait != null) {
@@ -421,6 +510,10 @@ public StringBuilder appendTo(StringBuilder builder) {
421510
}
422511
}
423512

513+
if (forUpdateBeforeOrderBy) {
514+
builder.append(orderByToString(oracleSiblings, orderByElements));
515+
}
516+
424517
return builder;
425518
}
426519

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,9 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
335335
unpivot.accept(this, context);
336336
}
337337

338-
deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
338+
if (!plainSelect.isForUpdateBeforeOrderBy()) {
339+
deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
340+
}
339341

340342
if (plainSelect.getForClause() != null) {
341343
plainSelect.getForClause().appendTo(builder);
@@ -363,8 +365,15 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
363365
builder.append(" FOR ");
364366
builder.append(plainSelect.getForMode().getValue());
365367

366-
if (plainSelect.getForUpdateTable() != null) {
367-
builder.append(" OF ").append(plainSelect.getForUpdateTable());
368+
List<Table> forUpdateTables = plainSelect.getForUpdateTables();
369+
if (forUpdateTables != null && !forUpdateTables.isEmpty()) {
370+
builder.append(" OF ");
371+
for (int i = 0; i < forUpdateTables.size(); i++) {
372+
if (i > 0) {
373+
builder.append(", ");
374+
}
375+
builder.append(forUpdateTables.get(i));
376+
}
368377
}
369378
if (plainSelect.getWait() != null) {
370379
// wait's toString will do the formatting for us
@@ -376,6 +385,10 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
376385
builder.append(" SKIP LOCKED");
377386
}
378387
}
388+
389+
if (plainSelect.isForUpdateBeforeOrderBy()) {
390+
deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
391+
}
379392
if (plainSelect.getMySqlSelectIntoClause() != null
380393
&& plainSelect.getMySqlSelectIntoClause()
381394
.getPosition() == MySqlSelectIntoClause.Position.TRAILING) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ public <S> Void visit(PlainSelect plainSelect, S context) {
101101

102102
validateOptionalFeature(c, plainSelect.getForUpdateTable(),
103103
Feature.selectForUpdateOfTable);
104+
if (plainSelect.getForUpdateTables() != null) {
105+
plainSelect.getForUpdateTables()
106+
.forEach(t -> validateOptionalFeature(c, t,
107+
Feature.selectForUpdateOfTable));
108+
}
104109
validateOptionalFeature(c, plainSelect.getWait(), Feature.selectForUpdateWait);
105110
validateFeature(c, plainSelect.isNoWait(), Feature.selectForUpdateNoWait);
106111
validateFeature(c, plainSelect.isSkipLocked(), Feature.selectForUpdateSkipLocked);

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4958,6 +4958,7 @@ PlainSelect PlainSelect() #PlainSelect:
49584958
List<Table> intoTables = null;
49594959
MySqlSelectIntoClause mySqlSelectIntoClause = null;
49604960
Table updateTable = null;
4961+
List<Table> updateTables = new ArrayList<Table>();
49614962
Wait wait = null;
49624963
boolean mySqlSqlCalcFoundRows = false;
49634964
Token token;
@@ -5084,10 +5085,17 @@ PlainSelect PlainSelect() #PlainSelect:
50845085
| (<K_READ> <K_ONLY> { plainSelect.setForMode(ForMode.READ_ONLY); })
50855086
| (<K_FETCH> <K_ONLY> { plainSelect.setForMode(ForMode.FETCH_ONLY); })
50865087
)
5087-
[ LOOKAHEAD(2) <K_OF> updateTable = Table() { plainSelect.setForUpdateTable(updateTable); } ]
5088+
[ LOOKAHEAD(2) <K_OF>
5089+
updateTable = Table() { updateTables.add(updateTable); }
5090+
( LOOKAHEAD(2) "," updateTable = Table() { updateTables.add(updateTable); } )*
5091+
{ plainSelect.setForUpdateTables(updateTables); }
5092+
]
50885093
[ LOOKAHEAD(<K_WAIT>) wait = Wait() { plainSelect.setWait(wait); } ]
50895094
[ LOOKAHEAD(2) (<K_NOWAIT> { plainSelect.setNoWait(true); }
50905095
| <K_SKIP> <K_LOCKED> { plainSelect.setSkipLocked(true); }) ]
5096+
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements()
5097+
{ plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); }
5098+
]
50915099
]
50925100
[ LOOKAHEAD(<K_INTO> (<K_OUTFILE> | <K_DUMPFILE>))
50935101
mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING)

0 commit comments

Comments
 (0)