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
5 changes: 4 additions & 1 deletion src/main/java/net/sf/jsqlparser/statement/lock/LockMode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
99 changes: 85 additions & 14 deletions src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,77 @@
*/
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.<br>
* Statement to lock one or more tables.<br>
* Example:<br>
* LOCK TABLE t IN EXCLUSIVE MODE<br>
* <br>
*/
public class LockStatement implements Statement {

private Table table;
private final List<Target> 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
*
* @param table The table to lock
* @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;
}
Expand All @@ -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<Target> 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;
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2745,7 +2745,9 @@ public void visit(Export export) {

@Override
public <S> Void visit(LockStatement lock, S context) {
lock.getTable().accept(this);
for (LockStatement.Target target : lock.getTargets()) {
target.getTable().accept(this, context);
}
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ public <S> StringBuilder visit(Export export, S context) {

@Override
public <S> StringBuilder visit(LockStatement lock, S context) {
builder.append(lock.toString());
lock.appendTo(builder);
return builder;
}

Expand Down
64 changes: 45 additions & 19 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -3532,27 +3532,53 @@ List<String> 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;
} {
<K_LOCK> <K_TABLE> table = Table() <K_IN>
(
LOOKAHEAD(2) ( <K_ROW> <K_SHARE> { lockMode = LockMode.RowShare; } ) |
( <K_ROW> <K_EXCLUSIVE> { lockMode = LockMode.RowExclusive; } ) |
LOOKAHEAD(2) ( <K_SHARE> <K_ROW> <K_EXCLUSIVE> { lockMode = LockMode.ShareRowExclusive; } ) |
LOOKAHEAD(2) ( <K_SHARE> <K_UPDATE> { lockMode = LockMode.ShareUpdate; } ) |
( <K_SHARE> { lockMode = LockMode.Share; } ) |
( <K_EXCLUSIVE > { lockMode = LockMode.Exclusive; } )
)
<K_MODE>
[ <K_NOWAIT> { noWait = true; } | <K_WAIT> waitSecondsToken = <S_LONG> { waitSeconds = Long.valueOf(waitSecondsToken.image); } ]
<K_LOCK>
{ statement.setUseTableKeyword(false); }
[ LOOKAHEAD(<K_TABLE>) <K_TABLE> { statement.setUseTableKeyword(true); } ]
target=LockTarget() { statement.getTargets().add(target); }
( "," target=LockTarget() { statement.getTargets().add(target); } )*
[ <K_IN> mode=TableLockMode() <K_MODE> { statement.setLockMode(mode); } ]
[ <K_NOWAIT> { statement.setNoWait(true); }
| <K_WAIT> waitSeconds=<S_LONG> { 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;
} {
[ <K_ONLY> { 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;
} {
(
<K_ROW> ( <K_SHARE> { mode = LockMode.RowShare; }
| <K_EXCLUSIVE> { mode = LockMode.RowExclusive; } )
| <K_SHARE> { mode = LockMode.Share; }
[ <K_ROW> <K_EXCLUSIVE> { mode = LockMode.ShareRowExclusive; }
| <K_UPDATE> { mode = LockMode.ShareUpdate; }
[ <K_EXCLUSIVE> { mode = LockMode.ShareUpdateExclusive; } ] ]
| <K_EXCLUSIVE> { mode = LockMode.Exclusive; }
| AccessKeyword("ACCESS") ( <K_SHARE> { mode = LockMode.AccessShare; }
| <K_EXCLUSIVE> { mode = LockMode.AccessExclusive; } )
)
{ return mode; }
}

LikeClause LikeClause(): {
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading