Skip to content

Commit f64ddd5

Browse files
authored
feat: model PostgreSQL LOCK targets and lock modes (#2668)
* feat: model PostgreSQL LOCK targets and lock modes Signed-off-by: minleejae <mmj9808@gmail.com> * style: declare lock statement fields before nested types Signed-off-by: minleejae <mmj9808@gmail.com> --------- Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 1eb5fc6 commit f64ddd5

6 files changed

Lines changed: 233 additions & 36 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/lock/LockMode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ public enum LockMode {
1818

1919
// These are Oracle specific, as far as I know
2020
RowShare("ROW SHARE"), RowExclusive("ROW EXCLUSIVE"), ShareUpdate(
21-
"SHARE UPDATE"), ShareRowExclusive("SHARE ROW EXCLUSIVE");
21+
"SHARE UPDATE"), ShareRowExclusive("SHARE ROW EXCLUSIVE"),
22+
23+
AccessShare("ACCESS SHARE"), AccessExclusive("ACCESS EXCLUSIVE"), ShareUpdateExclusive(
24+
"SHARE UPDATE EXCLUSIVE");
2225

2326
private final String value;
2427

src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,77 @@
99
*/
1010
package net.sf.jsqlparser.statement.lock;
1111

12+
import java.util.ArrayList;
13+
import java.util.List;
14+
import java.util.Objects;
1215
import net.sf.jsqlparser.schema.Table;
1316
import net.sf.jsqlparser.statement.Statement;
1417
import net.sf.jsqlparser.statement.StatementVisitor;
1518

1619
/**
17-
* Statement to Lock a specific table.<br>
20+
* Statement to lock one or more tables.<br>
1821
* Example:<br>
1922
* LOCK TABLE t IN EXCLUSIVE MODE<br>
2023
* <br>
2124
*/
2225
public class LockStatement implements Statement {
2326

24-
private Table table;
27+
private final List<Target> targets = new ArrayList<>();
28+
private boolean useTableKeyword = true;
2529
private LockMode lockMode;
2630
private boolean noWait;
2731
private Long waitSeconds;
2832

33+
public enum Scope {
34+
DEFAULT, ONLY, INCLUDING_DESCENDANTS
35+
}
36+
37+
public static class Target {
38+
private Table table;
39+
private Scope scope = Scope.DEFAULT;
40+
41+
public Target(Table table) {
42+
this.table = table;
43+
}
44+
45+
public Table getTable() {
46+
return table;
47+
}
48+
49+
public void setTable(Table table) {
50+
this.table = table;
51+
}
52+
53+
public Scope getScope() {
54+
return scope;
55+
}
56+
57+
public void setScope(Scope scope) {
58+
this.scope = Objects.requireNonNull(scope);
59+
}
60+
61+
@Override
62+
public String toString() {
63+
return (scope == Scope.ONLY ? "ONLY " : "") + table.getFullyQualifiedName()
64+
+ (scope == Scope.INCLUDING_DESCENDANTS ? " *" : "");
65+
}
66+
}
67+
68+
public LockStatement() {}
69+
2970
/**
3071
* Creates a new LockStatement
3172
*
3273
* @param table The table to lock
3374
* @param lockMode The lock mode
3475
*/
3576
public LockStatement(Table table, LockMode lockMode) {
36-
this.table = table;
77+
setTable(table);
3778
this.lockMode = lockMode;
3879
}
3980

4081
public LockStatement(Table table, LockMode lockMode, boolean noWait, Long waitSeconds) {
4182
this(table, lockMode);
42-
this.table = table;
43-
this.lockMode = lockMode;
4483
this.noWait = noWait;
4584
this.waitSeconds = waitSeconds;
4685
}
@@ -52,14 +91,33 @@ private void checkValidState() {
5291
}
5392
}
5493

94+
/** Returns the first target for compatibility with the single-table API. */
5595
public Table getTable() {
56-
return table;
96+
return targets.isEmpty() ? null : targets.get(0).getTable();
5797
}
5898

99+
/** Replaces the first target table while preserving its scope and any further targets. */
59100
public void setTable(Table table) {
60-
this.table = table;
101+
if (targets.isEmpty()) {
102+
targets.add(new Target(table));
103+
} else {
104+
targets.get(0).setTable(table);
105+
}
106+
}
107+
108+
public List<Target> getTargets() {
109+
return targets;
110+
}
111+
112+
public boolean isUseTableKeyword() {
113+
return useTableKeyword;
61114
}
62115

116+
public void setUseTableKeyword(boolean useTableKeyword) {
117+
this.useTableKeyword = useTableKeyword;
118+
}
119+
120+
/** Returns null when IN ... MODE was omitted. */
63121
public LockMode getLockMode() {
64122
return lockMode;
65123
}
@@ -104,15 +162,28 @@ public Long getWaitSeconds() {
104162
return waitSeconds;
105163
}
106164

165+
public StringBuilder appendTo(StringBuilder builder) {
166+
builder.append(useTableKeyword ? "LOCK TABLE " : "LOCK ");
167+
for (int i = 0; i < targets.size(); i++) {
168+
if (i > 0) {
169+
builder.append(", ");
170+
}
171+
builder.append(targets.get(i));
172+
}
173+
if (lockMode != null) {
174+
builder.append(" IN ").append(lockMode.getValue()).append(" MODE");
175+
}
176+
if (noWait) {
177+
builder.append(" NOWAIT");
178+
} else if (waitSeconds != null) {
179+
builder.append(" WAIT ").append(waitSeconds);
180+
}
181+
return builder;
182+
}
183+
107184
@Override
108185
public String toString() {
109-
return "LOCK TABLE "
110-
+ table.getFullyQualifiedName()
111-
+ " IN "
112-
+ lockMode.getValue()
113-
+ " MODE"
114-
+ (noWait ? " NOWAIT" : "")
115-
+ (waitSeconds != null ? " WAIT " + waitSeconds : "");
186+
return appendTo(new StringBuilder()).toString();
116187
}
117188

118189
@Override

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2745,7 +2745,9 @@ public void visit(Export export) {
27452745

27462746
@Override
27472747
public <S> Void visit(LockStatement lock, S context) {
2748-
lock.getTable().accept(this);
2748+
for (LockStatement.Target target : lock.getTargets()) {
2749+
target.getTable().accept(this, context);
2750+
}
27492751
return null;
27502752
}
27512753

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ public <S> StringBuilder visit(Export export, S context) {
692692

693693
@Override
694694
public <S> StringBuilder visit(LockStatement lock, S context) {
695-
builder.append(lock.toString());
695+
lock.appendTo(builder);
696696
return builder;
697697
}
698698

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

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3532,27 +3532,53 @@ List<String> error_skipto(int kind) {
35323532
}
35333533

35343534
LockStatement LockStatement(): {
3535-
Table table;
3536-
boolean noWait = false;
3537-
LockMode lockMode;
3538-
Token waitSecondsToken = null;
3539-
Long waitSeconds = null;
3535+
LockStatement statement = new LockStatement();
3536+
LockStatement.Target target;
3537+
LockMode mode;
3538+
Token waitSeconds;
35403539
} {
3541-
<K_LOCK> <K_TABLE> table = Table() <K_IN>
3542-
(
3543-
LOOKAHEAD(2) ( <K_ROW> <K_SHARE> { lockMode = LockMode.RowShare; } ) |
3544-
( <K_ROW> <K_EXCLUSIVE> { lockMode = LockMode.RowExclusive; } ) |
3545-
LOOKAHEAD(2) ( <K_SHARE> <K_ROW> <K_EXCLUSIVE> { lockMode = LockMode.ShareRowExclusive; } ) |
3546-
LOOKAHEAD(2) ( <K_SHARE> <K_UPDATE> { lockMode = LockMode.ShareUpdate; } ) |
3547-
( <K_SHARE> { lockMode = LockMode.Share; } ) |
3548-
( <K_EXCLUSIVE > { lockMode = LockMode.Exclusive; } )
3549-
)
3550-
<K_MODE>
3551-
[ <K_NOWAIT> { noWait = true; } | <K_WAIT> waitSecondsToken = <S_LONG> { waitSeconds = Long.valueOf(waitSecondsToken.image); } ]
3540+
<K_LOCK>
3541+
{ statement.setUseTableKeyword(false); }
3542+
[ LOOKAHEAD(<K_TABLE>) <K_TABLE> { statement.setUseTableKeyword(true); } ]
3543+
target=LockTarget() { statement.getTargets().add(target); }
3544+
( "," target=LockTarget() { statement.getTargets().add(target); } )*
3545+
[ <K_IN> mode=TableLockMode() <K_MODE> { statement.setLockMode(mode); } ]
3546+
[ <K_NOWAIT> { statement.setNoWait(true); }
3547+
| <K_WAIT> waitSeconds=<S_LONG> { statement.setWaitSeconds(Long.valueOf(waitSeconds.image)); } ]
3548+
{ return statement; }
3549+
}
35523550

3553-
{
3554-
return new LockStatement(table, lockMode, noWait, waitSeconds);
3555-
}
3551+
LockStatement.Target LockTarget(): {
3552+
Table table;
3553+
boolean only = false;
3554+
boolean descendants = false;
3555+
LockStatement.Target target;
3556+
} {
3557+
[ <K_ONLY> { only = true; } ] table=Table() [ "*" { descendants = true; } ]
3558+
{
3559+
requireDdlSyntax(!only || !descendants, "ONLY and * cannot describe the same lock target");
3560+
target = new LockStatement.Target(table);
3561+
target.setScope(only ? LockStatement.Scope.ONLY
3562+
: descendants ? LockStatement.Scope.INCLUDING_DESCENDANTS : LockStatement.Scope.DEFAULT);
3563+
return target;
3564+
}
3565+
}
3566+
3567+
LockMode TableLockMode(): {
3568+
LockMode mode;
3569+
} {
3570+
(
3571+
<K_ROW> ( <K_SHARE> { mode = LockMode.RowShare; }
3572+
| <K_EXCLUSIVE> { mode = LockMode.RowExclusive; } )
3573+
| <K_SHARE> { mode = LockMode.Share; }
3574+
[ <K_ROW> <K_EXCLUSIVE> { mode = LockMode.ShareRowExclusive; }
3575+
| <K_UPDATE> { mode = LockMode.ShareUpdate; }
3576+
[ <K_EXCLUSIVE> { mode = LockMode.ShareUpdateExclusive; } ] ]
3577+
| <K_EXCLUSIVE> { mode = LockMode.Exclusive; }
3578+
| AccessKeyword("ACCESS") ( <K_SHARE> { mode = LockMode.AccessShare; }
3579+
| <K_EXCLUSIVE> { mode = LockMode.AccessExclusive; } )
3580+
)
3581+
{ return mode; }
35563582
}
35573583

35583584
LikeClause LikeClause(): {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.lock;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.junit.jupiter.api.Assertions.*;
14+
import net.sf.jsqlparser.JSQLParserException;
15+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
16+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
17+
import net.sf.jsqlparser.schema.Table;
18+
import net.sf.jsqlparser.util.TablesNamesFinder;
19+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
20+
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.ValueSource;
23+
24+
class PostgreSqlLockTest {
25+
@ParameterizedTest
26+
@ValueSource(strings = {"ACCESS SHARE", "ROW SHARE", "ROW EXCLUSIVE",
27+
"SHARE UPDATE EXCLUSIVE", "SHARE", "SHARE ROW EXCLUSIVE", "EXCLUSIVE",
28+
"ACCESS EXCLUSIVE"})
29+
void supportsAllPostgreSqlModes(String mode) throws Exception {
30+
LockStatement lock = parse("LOCK TABLE a, public.b IN " + mode + " MODE NOWAIT");
31+
assertEquals(mode, lock.getLockMode().getValue());
32+
assertEquals(2, lock.getTargets().size());
33+
assertTrue(lock.isNoWait());
34+
roundTrip(lock);
35+
}
36+
37+
@Test
38+
void preservesScopesNamesAndOmittedKeywords() throws Exception {
39+
LockStatement lock = parse("LOCK ONLY \"a.b\", public.b * NOWAIT");
40+
assertFalse(lock.isUseTableKeyword());
41+
assertNull(lock.getLockMode());
42+
assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope());
43+
assertEquals(LockStatement.Scope.INCLUDING_DESCENDANTS,
44+
lock.getTargets().get(1).getScope());
45+
assertEquals("\"a.b\"", lock.getTable().getName());
46+
assertThat(new TablesNamesFinder().getTables(lock))
47+
.containsExactlyInAnyOrder("\"a.b\"", "public.b");
48+
roundTrip(lock);
49+
roundTrip(parse("LOCK TABLE a"));
50+
roundTrip(parse("LOCK a IN SHARE MODE"));
51+
}
52+
53+
@Test
54+
void legacyTableAccessorsRemainViewsOfTheFirstTarget() throws Exception {
55+
LockStatement lock = parse("LOCK TABLE ONLY a, b *");
56+
lock.getTargets().get(0).getTable().setName("changed");
57+
assertEquals("changed", lock.getTable().getName());
58+
lock.setTable(new Table("replacement"));
59+
assertEquals(2, lock.getTargets().size());
60+
assertEquals(LockStatement.Scope.ONLY, lock.getTargets().get(0).getScope());
61+
assertEquals("LOCK TABLE ONLY replacement, b *", lock.toString());
62+
roundTrip(lock);
63+
LockStatement legacy = new LockStatement(new Table("t"), LockMode.Exclusive, false, 5L);
64+
assertEquals("LOCK TABLE t IN EXCLUSIVE MODE WAIT 5", legacy.toString());
65+
assertThrows(IllegalStateException.class, () -> legacy.setNoWait(true));
66+
}
67+
68+
@Test
69+
void preservesOracleModeAndWait() throws Exception {
70+
String sql = "LOCK TABLE t IN SHARE UPDATE MODE WAIT 5";
71+
LockStatement lock = (LockStatement) CCJSqlParserUtil.parse(sql,
72+
p -> p.withDialect(Dialect.ORACLE));
73+
assertEquals(LockMode.ShareUpdate, lock.getLockMode());
74+
assertEquals(sql, lock.toString());
75+
}
76+
77+
@ParameterizedTest
78+
@ValueSource(strings = {"LOCK TABLE", "LOCK TABLE a,", "LOCK TABLE a IN ACCESS MODE",
79+
"LOCK TABLE a IN SHARE UPDATE EXCLUSIVE", "LOCK TABLE ONLY a *",
80+
"LOCK TABLE a NOWAIT WAIT 1"})
81+
void rejectsIncompleteAndConflictingClauses(String sql) {
82+
assertThrows(JSQLParserException.class, () -> parse(sql));
83+
}
84+
85+
private static LockStatement parse(String sql) throws JSQLParserException {
86+
return (LockStatement) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
87+
}
88+
89+
private static void roundTrip(LockStatement statement) throws Exception {
90+
StringBuilder out = new StringBuilder();
91+
statement.accept(new StatementDeParser(out), null);
92+
assertEquals(statement.toString(), out.toString());
93+
assertEquals(statement.toString(), parse(out.toString()).toString());
94+
}
95+
}

0 commit comments

Comments
 (0)