diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 2dd82f93e..cc880b585 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -44,6 +44,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp private Wait wait; private boolean noWait = false; private boolean forUpdateBeforeOrderBy = false; + private List additionalForUpdateClauses = null; Alias alias; Pivot pivot; UnPivot unPivot; @@ -416,6 +417,30 @@ public ForUpdateClause getForUpdate() { return clause; } + /** + * Returns the locking clauses that follow the first one, as in PostgreSQL's + * {@code FOR UPDATE OF t FOR SHARE OF u}, or {@code null} if there are none. The first clause + * is the one {@link #getForMode()}, {@link #getForUpdateTables()}, {@link #getWait()}, + * {@link #isNoWait()} and {@link #isSkipLocked()} describe. + * + * @return the locking clauses after the first one, or {@code null} + */ + public List getAdditionalForUpdateClauses() { + return additionalForUpdateClauses; + } + + public void setAdditionalForUpdateClauses(List additionalForUpdateClauses) { + this.additionalForUpdateClauses = additionalForUpdateClauses; + } + + public Select addAdditionalForUpdateClause(ForUpdateClause forUpdateClause) { + if (additionalForUpdateClauses == null) { + additionalForUpdateClauses = new ArrayList<>(); + } + additionalForUpdateClauses.add(forUpdateClause); + return this; + } + /** * 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). @@ -572,6 +597,11 @@ public StringBuilder appendTo(StringBuilder builder) { } else if (isSkipLocked()) { builder.append(" SKIP LOCKED"); } + if (additionalForUpdateClauses != null) { + for (ForUpdateClause forUpdateClause : additionalForUpdateClauses) { + forUpdateClause.appendTo(builder); + } + } } if (forUpdateBeforeOrderBy) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 2e293b7d2..47ea406c2 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -50,6 +50,7 @@ import net.sf.jsqlparser.statement.select.Distinct; import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.First; +import net.sf.jsqlparser.statement.select.ForUpdateClause; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.FromItemVisitor; import net.sf.jsqlparser.statement.select.InterpolateElement; @@ -313,6 +314,12 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { } else if (plainSelect.isSkipLocked()) { builder.append(" SKIP LOCKED"); } + if (plainSelect.getAdditionalForUpdateClauses() != null) { + for (ForUpdateClause forUpdateClause : plainSelect + .getAdditionalForUpdateClauses()) { + forUpdateClause.appendTo(builder); + } + } } if (plainSelect.isForUpdateBeforeOrderBy()) { diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index 43cc70d74..968162287 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -23,6 +23,7 @@ import net.sf.jsqlparser.statement.select.ExceptOp; import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.ForMode; +import net.sf.jsqlparser.statement.select.ForUpdateClause; import net.sf.jsqlparser.statement.select.FromItemVisitor; import net.sf.jsqlparser.statement.select.InterpolateElement; import net.sf.jsqlparser.statement.select.IntersectOp; @@ -142,6 +143,29 @@ public Void visit(PlainSelect plainSelect, S context) { validateOptionalFeature(c, plainSelect.getWait(), Feature.selectForUpdateWait); validateFeature(c, plainSelect.isNoWait(), Feature.selectForUpdateNoWait); validateFeature(c, plainSelect.isSkipLocked(), Feature.selectForUpdateSkipLocked); + + if (plainSelect.getAdditionalForUpdateClauses() != null) { + for (ForUpdateClause forUpdateClause : plainSelect + .getAdditionalForUpdateClauses()) { + validateFeature(c, forUpdateClause.getMode() == ForMode.KEY_SHARE, + Feature.selectForKeyShare); + validateFeature(c, forUpdateClause.getMode() == ForMode.NO_KEY_UPDATE, + Feature.selectForNoKeyUpdate); + validateFeature(c, forUpdateClause.getMode() == ForMode.SHARE, + Feature.selectForShare); + if (forUpdateClause.getTables() != null) { + forUpdateClause.getTables() + .forEach(t -> validateOptionalFeature(c, t, + Feature.selectForUpdateOfTable)); + } + validateOptionalFeature(c, forUpdateClause.getWait(), + Feature.selectForUpdateWait); + validateFeature(c, forUpdateClause.isNoWait(), + Feature.selectForUpdateNoWait); + validateFeature(c, forUpdateClause.isSkipLocked(), + Feature.selectForUpdateSkipLocked); + } + } } validateOptionalFeature(c, plainSelect.getForXmlPath(), Feature.selectForXmlPath); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f21f6617b..5f7997281 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -12517,6 +12517,34 @@ void MySqlSelectModifier(PlainSelect select): } } +ForUpdateClause AdditionalForUpdateClause(): +{ + ForUpdateClause clause = new ForUpdateClause(); + List tables = new ArrayList
(); + Table table; + Wait wait; +} +{ + + ( + { clause.setMode(ForMode.UPDATE); } + | { clause.setMode(ForMode.SHARE); } + | ( { clause.setMode(ForMode.NO_KEY_UPDATE); }) + | ( { clause.setMode(ForMode.KEY_SHARE); }) + ) + [ LOOKAHEAD(2) + table = Table() { tables.add(table); } + ( LOOKAHEAD(2) "," table = Table() { tables.add(table); } )* + { clause.setTables(tables); } + ] + [ LOOKAHEAD(1, ) wait = Wait() { clause.setWait(wait); } ] + [ LOOKAHEAD(2) ( { clause.setNoWait(true); } + | { clause.setSkipLocked(true); }) ] + { + return clause; + } +} + ForClause ForClause() #ForClause: { Token token = null; @@ -12653,6 +12681,7 @@ PlainSelect PlainSelect() #PlainSelect: MySqlProcedureAnalyse mySqlProcedureAnalyse = null; Table updateTable = null; List
updateTables = new ArrayList
(); + ForUpdateClause additionalForUpdateClause; Wait wait = null; Token token; KSQLWindow ksqlWindow = null; @@ -12819,6 +12848,11 @@ PlainSelect PlainSelect() #PlainSelect: [ LOOKAHEAD(1, ) wait = Wait() { plainSelect.setWait(wait); } ] [ LOOKAHEAD(2) ( { plainSelect.setNoWait(true); } | { plainSelect.setSkipLocked(true); }) ] + // PostgreSQL: more locking clauses, e.g. FOR UPDATE OF t FOR SHARE OF u + ( LOOKAHEAD( ( | | | )) + additionalForUpdateClause = AdditionalForUpdateClause() + { plainSelect.addAdditionalForUpdateClause(additionalForUpdateClause); } + )* [ LOOKAHEAD( ) orderByElements = OrderByElements() { plainSelect.setOrderByElements(orderByElements); plainSelect.setForUpdateBeforeOrderBy(true); } ] diff --git a/src/test/java/net/sf/jsqlparser/statement/select/ForUpdateTest.java b/src/test/java/net/sf/jsqlparser/statement/select/ForUpdateTest.java index 23eb3d229..f082ea458 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/ForUpdateTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/ForUpdateTest.java @@ -11,10 +11,13 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; public class ForUpdateTest { @@ -145,4 +148,50 @@ void testForUpdateWait() throws JSQLParserException { assertThat(forUpdate.getWait()).isNotNull(); assertThat(forUpdate.getWait().getTimeout()).isEqualTo(10L); } + + @Test + void testPostgresMultipleLockingClausesIssue2926() throws JSQLParserException { + String sqlStr = "SELECT * FROM t JOIN u ON u.t_id = t.id FOR UPDATE OF t FOR SHARE OF u"; + Statement stmt = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + PlainSelect plainSelect = (PlainSelect) stmt; + + assertThat(plainSelect.getForMode()).isEqualTo(ForMode.UPDATE); + assertThat(plainSelect.getForUpdateTables()).extracting(Table::getName) + .containsExactly("t"); + + assertThat(plainSelect.getAdditionalForUpdateClauses()).hasSize(1); + ForUpdateClause second = plainSelect.getAdditionalForUpdateClauses().get(0); + assertThat(second.isForShare()).isTrue(); + assertThat(second.getTables()).extracting(Table::getName).containsExactly("u"); + } + + @Test + void testPostgresMultipleLockingClausesWithOptions() throws JSQLParserException { + String sqlStr = "SELECT * FROM a, b, c, d" + + " FOR NO KEY UPDATE OF a NOWAIT" + + " FOR KEY SHARE OF b, c SKIP LOCKED" + + " FOR SHARE" + + " FOR UPDATE OF d NOWAIT"; + Statement stmt = TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + PlainSelect plainSelect = (PlainSelect) stmt; + + assertThat(plainSelect.getForMode()).isEqualTo(ForMode.NO_KEY_UPDATE); + assertThat(plainSelect.isNoWait()).isTrue(); + + List more = plainSelect.getAdditionalForUpdateClauses(); + assertThat(more).extracting(ForUpdateClause::getMode) + .containsExactly(ForMode.KEY_SHARE, ForMode.SHARE, ForMode.UPDATE); + assertThat(more.get(0).getTables()).extracting(Table::getName) + .containsExactly("b", "c"); + assertThat(more.get(0).isSkipLocked()).isTrue(); + assertThat(more.get(1).getTables()).isNull(); + assertThat(more.get(2).isNoWait()).isTrue(); + } + + @Test + void testSingleLockingClauseHasNoAdditionalClauses() throws JSQLParserException { + PlainSelect plainSelect = + (PlainSelect) CCJSqlParserUtil.parse("SELECT * FROM t FOR UPDATE OF t"); + assertThat(plainSelect.getAdditionalForUpdateClauses()).isNull(); + } } diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/SelectValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/SelectValidatorTest.java index c6ffec2fd..5a7856166 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/SelectValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/SelectValidatorTest.java @@ -121,6 +121,16 @@ public void testValidationForPostgresShare() throws JSQLParserException { Feature.selectForNoKeyUpdate); } + @Test + public void testValidationMultipleLockingClauses() throws JSQLParserException { + String sql = "SELECT * FROM t, u FOR UPDATE OF t FOR SHARE OF u"; + validateNoErrors(sql, 1, DatabaseType.POSTGRESQL); + + String sql2 = "SELECT * FROM t FOR UPDATE FOR NO KEY UPDATE"; + validateNotSupported(sql2, 1, 1, DatabaseType.ORACLE, + Feature.selectForNoKeyUpdate); + } + @Test public void testValidationForUpdateNoWait() throws JSQLParserException { String sql = "SELECT * FROM mytable FOR UPDATE NOWAIT";