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
135 changes: 135 additions & 0 deletions src/main/java/net/sf/jsqlparser/statement/select/ForUpdateClause.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2024 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.select;

import java.util.List;
import net.sf.jsqlparser.schema.Table;

/**
* Represents a FOR UPDATE / FOR SHARE locking clause in a SELECT statement.
*
* <p>
* Supports all common SQL dialects:
* <ul>
* <li>{@code FOR UPDATE} – standard row locking</li>
* <li>{@code FOR UPDATE OF t1, t2} – table-specific locking (Oracle, PostgreSQL)</li>
* <li>{@code FOR UPDATE NOWAIT} – fail immediately if rows are locked (Oracle, PostgreSQL)</li>
* <li>{@code FOR UPDATE WAIT n} – wait up to n seconds (Oracle)</li>
* <li>{@code FOR UPDATE SKIP LOCKED} – skip locked rows (Oracle, PostgreSQL)</li>
* <li>{@code FOR SHARE} – shared row lock (PostgreSQL)</li>
* <li>{@code FOR KEY SHARE} – key-level shared lock (PostgreSQL)</li>
* <li>{@code FOR NO KEY UPDATE} – non-key exclusive lock (PostgreSQL)</li>
* </ul>
* </p>
*/
public class ForUpdateClause {

private ForMode mode;
private List<Table> tables;
private Wait wait;
private boolean noWait;
private boolean skipLocked;

public ForMode getMode() {
return mode;
}

public ForUpdateClause setMode(ForMode mode) {
this.mode = mode;
return this;
}

public List<Table> getTables() {
return tables;
}

public ForUpdateClause setTables(List<Table> tables) {
this.tables = tables;
return this;
}

/**
* Returns the first table from the OF clause, or {@code null} if none was specified.
*
* @return the first table, or {@code null}
*/
public Table getFirstTable() {
return (tables != null && !tables.isEmpty()) ? tables.get(0) : null;
}

public Wait getWait() {
return wait;
}

public ForUpdateClause setWait(Wait wait) {
this.wait = wait;
return this;
}

public boolean isNoWait() {
return noWait;
}

public ForUpdateClause setNoWait(boolean noWait) {
this.noWait = noWait;
return this;
}

public boolean isSkipLocked() {
return skipLocked;
}

public ForUpdateClause setSkipLocked(boolean skipLocked) {
this.skipLocked = skipLocked;
return this;
}

/** Returns {@code true} when the mode is {@link ForMode#UPDATE}. */
public boolean isForUpdate() {
return mode == ForMode.UPDATE;
}

/** Returns {@code true} when the mode is {@link ForMode#SHARE}. */
public boolean isForShare() {
return mode == ForMode.SHARE;
}

/** Returns {@code true} when at least one table was listed in the OF clause. */
public boolean hasTableList() {
return tables != null && !tables.isEmpty();
}

public StringBuilder appendTo(StringBuilder builder) {
builder.append(" FOR ").append(mode.getValue());
if (tables != null && !tables.isEmpty()) {
builder.append(" OF ");
for (int i = 0; i < tables.size(); i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(tables.get(i));
}
}
if (wait != null) {
builder.append(wait);
}
if (noWait) {
builder.append(" NOWAIT");
} else if (skipLocked) {
builder.append(" SKIP LOCKED");
}
return builder;
}

@Override
public String toString() {
return appendTo(new StringBuilder()).toString();
}
}
105 changes: 99 additions & 6 deletions src/main/java/net/sf/jsqlparser/statement/select/Select.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import net.sf.jsqlparser.statement.StatementVisitor;

public abstract class Select extends ASTNodeAccessImpl implements Statement, Expression, FromItem {
protected Table forUpdateTable = null;
protected List<Table> forUpdateTables = null;
protected List<WithItem<?>> withItemsList;
Limit limitBy;
Limit limit;
Expand All @@ -40,6 +40,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp
private boolean skipLocked;
private Wait wait;
private boolean noWait = false;
private boolean forUpdateBeforeOrderBy = false;
Alias alias;
Pivot pivot;
UnPivot unPivot;
Expand Down Expand Up @@ -291,12 +292,92 @@ public void setForMode(ForMode forMode) {
this.forMode = forMode;
}

/**
* Returns the first table from the {@code FOR UPDATE OF} clause, or {@code null} if no table
* was specified. Use {@link #getForUpdateTables()} to retrieve all tables.
*
* @return the first table, or {@code null}
*/
public Table getForUpdateTable() {
return this.forUpdateTable;
return (forUpdateTables != null && !forUpdateTables.isEmpty()) ? forUpdateTables.get(0)
: null;
}

/**
* Sets a single table for the {@code FOR UPDATE OF} clause.
*
* @param forUpdateTable the table, or {@code null} to clear
*/
public void setForUpdateTable(Table forUpdateTable) {
this.forUpdateTable = forUpdateTable;
if (forUpdateTable == null) {
this.forUpdateTables = null;
} else {
this.forUpdateTables = new ArrayList<>();
this.forUpdateTables.add(forUpdateTable);
}
}

/**
* Returns the list of tables named in the {@code FOR UPDATE OF t1, t2, ...} clause, or
* {@code null} if no OF clause was present.
*
* @return list of tables, or {@code null}
*/
public List<Table> getForUpdateTables() {
return forUpdateTables;
}

/**
* Sets the list of tables for the {@code FOR UPDATE OF t1, t2, ...} clause.
*
* @param forUpdateTables list of tables
*/
public void setForUpdateTables(List<Table> forUpdateTables) {
this.forUpdateTables = forUpdateTables;
}

public Select withForUpdateTables(List<Table> forUpdateTables) {
this.setForUpdateTables(forUpdateTables);
return this;
}

/**
* Builds and returns a {@link ForUpdateClause} representing the current FOR UPDATE / FOR SHARE
* state of this SELECT, or {@code null} if no FOR clause is present.
*
* @return a {@link ForUpdateClause} view, or {@code null}
*/
public ForUpdateClause getForUpdate() {
if (forMode == null) {
return null;
}
ForUpdateClause clause = new ForUpdateClause();
clause.setMode(forMode);
clause.setTables(forUpdateTables);
clause.setWait(wait);
clause.setNoWait(noWait);
clause.setSkipLocked(skipLocked);
return clause;
}

/**
* Returns {@code true} when the {@code FOR UPDATE} clause appears before the {@code ORDER BY}
* clause in the original SQL (non-standard ordering supported by some databases).
*
* @return {@code true} if FOR UPDATE precedes ORDER BY
*/
public boolean isForUpdateBeforeOrderBy() {
return forUpdateBeforeOrderBy;
}

/**
* Indicates whether the {@code FOR UPDATE} clause precedes the {@code ORDER BY} clause in the
* SQL output.
*
* @param forUpdateBeforeOrderBy {@code true} to emit FOR UPDATE before ORDER BY
*/
public void setForUpdateBeforeOrderBy(boolean forUpdateBeforeOrderBy) {
this.forUpdateBeforeOrderBy = forUpdateBeforeOrderBy;
}

/**
Expand Down Expand Up @@ -380,7 +461,9 @@ public StringBuilder appendTo(StringBuilder builder) {

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

builder.append(orderByToString(oracleSiblings, orderByElements));
if (!forUpdateBeforeOrderBy) {
builder.append(orderByToString(oracleSiblings, orderByElements));
}

if (forClause != null) {
forClause.appendTo(builder);
Expand All @@ -405,8 +488,14 @@ public StringBuilder appendTo(StringBuilder builder) {
builder.append(" FOR ");
builder.append(forMode.getValue());

if (getForUpdateTable() != null) {
builder.append(" OF ").append(forUpdateTable);
if (forUpdateTables != null && !forUpdateTables.isEmpty()) {
builder.append(" OF ");
for (int i = 0; i < forUpdateTables.size(); i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(forUpdateTables.get(i));
}
}

if (wait != null) {
Expand All @@ -421,6 +510,10 @@ public StringBuilder appendTo(StringBuilder builder) {
}
}

if (forUpdateBeforeOrderBy) {
builder.append(orderByToString(oracleSiblings, orderByElements));
}

return builder;
}

Expand Down
19 changes: 16 additions & 3 deletions src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
unpivot.accept(this, context);
}

deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
if (!plainSelect.isForUpdateBeforeOrderBy()) {
deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
}

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

if (plainSelect.getForUpdateTable() != null) {
builder.append(" OF ").append(plainSelect.getForUpdateTable());
List<Table> forUpdateTables = plainSelect.getForUpdateTables();
if (forUpdateTables != null && !forUpdateTables.isEmpty()) {
builder.append(" OF ");
for (int i = 0; i < forUpdateTables.size(); i++) {
if (i > 0) {
builder.append(", ");
}
builder.append(forUpdateTables.get(i));
}
}
if (plainSelect.getWait() != null) {
// wait's toString will do the formatting for us
Expand All @@ -376,6 +385,10 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
builder.append(" SKIP LOCKED");
}
}

if (plainSelect.isForUpdateBeforeOrderBy()) {
deparseOrderByElementsClause(plainSelect, plainSelect.getOrderByElements());
}
if (plainSelect.getMySqlSelectIntoClause() != null
&& plainSelect.getMySqlSelectIntoClause()
.getPosition() == MySqlSelectIntoClause.Position.TRAILING) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public <S> Void visit(PlainSelect plainSelect, S context) {

validateOptionalFeature(c, plainSelect.getForUpdateTable(),
Feature.selectForUpdateOfTable);
if (plainSelect.getForUpdateTables() != null) {
plainSelect.getForUpdateTables()
.forEach(t -> validateOptionalFeature(c, t,
Feature.selectForUpdateOfTable));
}
validateOptionalFeature(c, plainSelect.getWait(), Feature.selectForUpdateWait);
validateFeature(c, plainSelect.isNoWait(), Feature.selectForUpdateNoWait);
validateFeature(c, plainSelect.isSkipLocked(), Feature.selectForUpdateSkipLocked);
Expand Down
10 changes: 9 additions & 1 deletion src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -4958,6 +4958,7 @@ PlainSelect PlainSelect() #PlainSelect:
List<Table> intoTables = null;
MySqlSelectIntoClause mySqlSelectIntoClause = null;
Table updateTable = null;
List<Table> updateTables = new ArrayList<Table>();
Wait wait = null;
boolean mySqlSqlCalcFoundRows = false;
Token token;
Expand Down Expand Up @@ -5084,10 +5085,17 @@ PlainSelect PlainSelect() #PlainSelect:
| (<K_READ> <K_ONLY> { plainSelect.setForMode(ForMode.READ_ONLY); })
| (<K_FETCH> <K_ONLY> { plainSelect.setForMode(ForMode.FETCH_ONLY); })
)
[ LOOKAHEAD(2) <K_OF> updateTable = Table() { plainSelect.setForUpdateTable(updateTable); } ]
[ LOOKAHEAD(2) <K_OF>
updateTable = Table() { updateTables.add(updateTable); }
( LOOKAHEAD(2) "," updateTable = Table() { updateTables.add(updateTable); } )*
{ plainSelect.setForUpdateTables(updateTables); }
]
[ LOOKAHEAD(<K_WAIT>) wait = Wait() { plainSelect.setWait(wait); } ]
[ LOOKAHEAD(2) (<K_NOWAIT> { plainSelect.setNoWait(true); }
| <K_SKIP> <K_LOCKED> { plainSelect.setSkipLocked(true); }) ]
[ LOOKAHEAD(<K_ORDER> <K_BY>) orderByElements = OrderByElements()
{ plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); }
]
]
[ LOOKAHEAD(<K_INTO> (<K_OUTFILE> | <K_DUMPFILE>))
mySqlSelectIntoClause = MySqlSelectIntoClause(MySqlSelectIntoClause.Position.TRAILING)
Expand Down
Loading