diff --git a/src/main/java/net/sf/jsqlparser/statement/lock/LockMode.java b/src/main/java/net/sf/jsqlparser/statement/lock/LockMode.java
index be372218ed..928128e0c1 100644
--- a/src/main/java/net/sf/jsqlparser/statement/lock/LockMode.java
+++ b/src/main/java/net/sf/jsqlparser/statement/lock/LockMode.java
@@ -18,7 +18,10 @@ public enum LockMode {
// These are Oracle specific, as far as I know
RowShare("ROW SHARE"), RowExclusive("ROW EXCLUSIVE"), ShareUpdate(
- "SHARE UPDATE"), ShareRowExclusive("SHARE ROW EXCLUSIVE");
+ "SHARE UPDATE"), ShareRowExclusive("SHARE ROW EXCLUSIVE"),
+
+ AccessShare("ACCESS SHARE"), AccessExclusive("ACCESS EXCLUSIVE"), ShareUpdateExclusive(
+ "SHARE UPDATE EXCLUSIVE");
private final String value;
diff --git a/src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java b/src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java
index 2ec25e2204..ffdcbed52d 100644
--- a/src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java
@@ -9,23 +9,64 @@
*/
package net.sf.jsqlparser.statement.lock;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.StatementVisitor;
/**
- * Statement to Lock a specific table.
+ * Statement to lock one or more tables.
* Example:
* LOCK TABLE t IN EXCLUSIVE MODE
*
*/
public class LockStatement implements Statement {
- private Table table;
+ private final List targets = new ArrayList<>();
+ private boolean useTableKeyword = true;
private LockMode lockMode;
private boolean noWait;
private Long waitSeconds;
+ public enum Scope {
+ DEFAULT, ONLY, INCLUDING_DESCENDANTS
+ }
+
+ public static class Target {
+ private Table table;
+ private Scope scope = Scope.DEFAULT;
+
+ public Target(Table table) {
+ this.table = table;
+ }
+
+ public Table getTable() {
+ return table;
+ }
+
+ public void setTable(Table table) {
+ this.table = table;
+ }
+
+ public Scope getScope() {
+ return scope;
+ }
+
+ public void setScope(Scope scope) {
+ this.scope = Objects.requireNonNull(scope);
+ }
+
+ @Override
+ public String toString() {
+ return (scope == Scope.ONLY ? "ONLY " : "") + table.getFullyQualifiedName()
+ + (scope == Scope.INCLUDING_DESCENDANTS ? " *" : "");
+ }
+ }
+
+ public LockStatement() {}
+
/**
* Creates a new LockStatement
*
@@ -33,14 +74,12 @@ public class LockStatement implements Statement {
* @param lockMode The lock mode
*/
public LockStatement(Table table, LockMode lockMode) {
- this.table = table;
+ setTable(table);
this.lockMode = lockMode;
}
public LockStatement(Table table, LockMode lockMode, boolean noWait, Long waitSeconds) {
this(table, lockMode);
- this.table = table;
- this.lockMode = lockMode;
this.noWait = noWait;
this.waitSeconds = waitSeconds;
}
@@ -52,14 +91,33 @@ private void checkValidState() {
}
}
+ /** Returns the first target for compatibility with the single-table API. */
public Table getTable() {
- return table;
+ return targets.isEmpty() ? null : targets.get(0).getTable();
}
+ /** Replaces the first target table while preserving its scope and any further targets. */
public void setTable(Table table) {
- this.table = table;
+ if (targets.isEmpty()) {
+ targets.add(new Target(table));
+ } else {
+ targets.get(0).setTable(table);
+ }
+ }
+
+ public List getTargets() {
+ return targets;
+ }
+
+ public boolean isUseTableKeyword() {
+ return useTableKeyword;
}
+ public void setUseTableKeyword(boolean useTableKeyword) {
+ this.useTableKeyword = useTableKeyword;
+ }
+
+ /** Returns null when IN ... MODE was omitted. */
public LockMode getLockMode() {
return lockMode;
}
@@ -104,15 +162,28 @@ public Long getWaitSeconds() {
return waitSeconds;
}
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append(useTableKeyword ? "LOCK TABLE " : "LOCK ");
+ for (int i = 0; i < targets.size(); i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+ builder.append(targets.get(i));
+ }
+ if (lockMode != null) {
+ builder.append(" IN ").append(lockMode.getValue()).append(" MODE");
+ }
+ if (noWait) {
+ builder.append(" NOWAIT");
+ } else if (waitSeconds != null) {
+ builder.append(" WAIT ").append(waitSeconds);
+ }
+ return builder;
+ }
+
@Override
public String toString() {
- return "LOCK TABLE "
- + table.getFullyQualifiedName()
- + " IN "
- + lockMode.getValue()
- + " MODE"
- + (noWait ? " NOWAIT" : "")
- + (waitSeconds != null ? " WAIT " + waitSeconds : "");
+ return appendTo(new StringBuilder()).toString();
}
@Override
diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
index 58ed96435c..afed50466d 100644
--- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
+++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
@@ -2745,7 +2745,9 @@ public void visit(Export export) {
@Override
public Void visit(LockStatement lock, S context) {
- lock.getTable().accept(this);
+ for (LockStatement.Target target : lock.getTargets()) {
+ target.getTable().accept(this, context);
+ }
return null;
}
diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
index b4ce34096a..889e0dac66 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
@@ -692,7 +692,7 @@ public StringBuilder visit(Export export, S context) {
@Override
public StringBuilder visit(LockStatement lock, S context) {
- builder.append(lock.toString());
+ lock.appendTo(builder);
return builder;
}
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 6fd8d0375c..cda499bb6b 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -3532,27 +3532,53 @@ List error_skipto(int kind) {
}
LockStatement LockStatement(): {
- Table table;
- boolean noWait = false;
- LockMode lockMode;
- Token waitSecondsToken = null;
- Long waitSeconds = null;
+ LockStatement statement = new LockStatement();
+ LockStatement.Target target;
+ LockMode mode;
+ Token waitSeconds;
} {
- table = Table()
- (
- LOOKAHEAD(2) ( { lockMode = LockMode.RowShare; } ) |
- ( { lockMode = LockMode.RowExclusive; } ) |
- LOOKAHEAD(2) ( { lockMode = LockMode.ShareRowExclusive; } ) |
- LOOKAHEAD(2) ( { lockMode = LockMode.ShareUpdate; } ) |
- ( { lockMode = LockMode.Share; } ) |
- ( { lockMode = LockMode.Exclusive; } )
- )
-
- [ { noWait = true; } | waitSecondsToken = { waitSeconds = Long.valueOf(waitSecondsToken.image); } ]
+
+ { statement.setUseTableKeyword(false); }
+ [ LOOKAHEAD() { statement.setUseTableKeyword(true); } ]
+ target=LockTarget() { statement.getTargets().add(target); }
+ ( "," target=LockTarget() { statement.getTargets().add(target); } )*
+ [ mode=TableLockMode() { statement.setLockMode(mode); } ]
+ [ { statement.setNoWait(true); }
+ | waitSeconds= { statement.setWaitSeconds(Long.valueOf(waitSeconds.image)); } ]
+ { return statement; }
+}
- {
- return new LockStatement(table, lockMode, noWait, waitSeconds);
- }
+LockStatement.Target LockTarget(): {
+ Table table;
+ boolean only = false;
+ boolean descendants = false;
+ LockStatement.Target target;
+} {
+ [ { only = true; } ] table=Table() [ "*" { descendants = true; } ]
+ {
+ requireDdlSyntax(!only || !descendants, "ONLY and * cannot describe the same lock target");
+ target = new LockStatement.Target(table);
+ target.setScope(only ? LockStatement.Scope.ONLY
+ : descendants ? LockStatement.Scope.INCLUDING_DESCENDANTS : LockStatement.Scope.DEFAULT);
+ return target;
+ }
+}
+
+LockMode TableLockMode(): {
+ LockMode mode;
+} {
+ (
+ ( { mode = LockMode.RowShare; }
+ | { mode = LockMode.RowExclusive; } )
+ | { mode = LockMode.Share; }
+ [ { mode = LockMode.ShareRowExclusive; }
+ | { mode = LockMode.ShareUpdate; }
+ [ { mode = LockMode.ShareUpdateExclusive; } ] ]
+ | { mode = LockMode.Exclusive; }
+ | AccessKeyword("ACCESS") ( { mode = LockMode.AccessShare; }
+ | { mode = LockMode.AccessExclusive; } )
+ )
+ { return mode; }
}
LikeClause LikeClause(): {
diff --git a/src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java b/src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java
new file mode 100644
index 0000000000..f5897e3ca1
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java
@@ -0,0 +1,95 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2026 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.lock;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class PostgreSqlLockTest {
+ @ParameterizedTest
+ @ValueSource(strings = {"ACCESS SHARE", "ROW SHARE", "ROW EXCLUSIVE",
+ "SHARE UPDATE EXCLUSIVE", "SHARE", "SHARE ROW EXCLUSIVE", "EXCLUSIVE",
+ "ACCESS EXCLUSIVE"})
+ void supportsAllPostgreSqlModes(String mode) throws Exception {
+ LockStatement lock = parse("LOCK TABLE a, public.b IN " + mode + " MODE NOWAIT");
+ assertEquals(mode, lock.getLockMode().getValue());
+ assertEquals(2, lock.getTargets().size());
+ assertTrue(lock.isNoWait());
+ roundTrip(lock);
+ }
+
+ @Test
+ void preservesScopesNamesAndOmittedKeywords() throws Exception {
+ LockStatement lock = parse("LOCK ONLY \"a.b\", public.b * NOWAIT");
+ assertFalse(lock.isUseTableKeyword());
+ assertNull(lock.getLockMode());
+ assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope());
+ assertEquals(LockStatement.Scope.INCLUDING_DESCENDANTS,
+ lock.getTargets().get(1).getScope());
+ assertEquals("\"a.b\"", lock.getTable().getName());
+ assertThat(new TablesNamesFinder().getTables(lock))
+ .containsExactlyInAnyOrder("\"a.b\"", "public.b");
+ roundTrip(lock);
+ roundTrip(parse("LOCK TABLE a"));
+ roundTrip(parse("LOCK a IN SHARE MODE"));
+ }
+
+ @Test
+ void legacyTableAccessorsRemainViewsOfTheFirstTarget() throws Exception {
+ LockStatement lock = parse("LOCK TABLE ONLY a, b *");
+ lock.getTargets().get(0).getTable().setName("changed");
+ assertEquals("changed", lock.getTable().getName());
+ lock.setTable(new Table("replacement"));
+ assertEquals(2, lock.getTargets().size());
+ assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope());
+ assertEquals("LOCK TABLE ONLY replacement, b *", lock.toString());
+ roundTrip(lock);
+ LockStatement legacy = new LockStatement(new Table("t"), LockMode.Exclusive, false, 5L);
+ assertEquals("LOCK TABLE t IN EXCLUSIVE MODE WAIT 5", legacy.toString());
+ assertThrows(IllegalStateException.class, () -> legacy.setNoWait(true));
+ }
+
+ @Test
+ void preservesOracleModeAndWait() throws Exception {
+ String sql = "LOCK TABLE t IN SHARE UPDATE MODE WAIT 5";
+ LockStatement lock = (LockStatement) CCJSqlParserUtil.parse(sql,
+ p -> p.withDialect(Dialect.ORACLE));
+ assertEquals(LockMode.ShareUpdate, lock.getLockMode());
+ assertEquals(sql, lock.toString());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"LOCK TABLE", "LOCK TABLE a,", "LOCK TABLE a IN ACCESS MODE",
+ "LOCK TABLE a IN SHARE UPDATE EXCLUSIVE", "LOCK TABLE ONLY a *",
+ "LOCK TABLE a NOWAIT WAIT 1"})
+ void rejectsIncompleteAndConflictingClauses(String sql) {
+ assertThrows(JSQLParserException.class, () -> parse(sql));
+ }
+
+ private static LockStatement parse(String sql) throws JSQLParserException {
+ return (LockStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
+ }
+
+ private static void roundTrip(LockStatement statement) throws Exception {
+ StringBuilder out = new StringBuilder();
+ statement.accept(new StatementDeParser(out), null);
+ assertEquals(statement.toString(), out.toString());
+ assertEquals(statement.toString(), parse(out.toString()).toString());
+ }
+}