From c8ef0c1c8402f49a4e95326afab972dc85775233 Mon Sep 17 00:00:00 2001 From: minleejae Date: Mon, 21 Sep 2026 23:19:08 +0900 Subject: [PATCH 1/2] feat: model PostgreSQL LOCK targets and lock modes Signed-off-by: minleejae --- .../jsqlparser/statement/lock/LockMode.java | 5 +- .../statement/lock/LockStatement.java | 99 ++++++++++++++++--- .../sf/jsqlparser/util/TablesNamesFinder.java | 4 +- .../util/deparser/StatementDeParser.java | 2 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 64 ++++++++---- .../statement/lock/PostgreSqlLockTest.java | 95 ++++++++++++++++++ 6 files changed, 233 insertions(+), 36 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/lock/PostgreSqlLockTest.java 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..3b2f873d5f 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; + 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 ? " *" : ""); + } + } + + private final List targets = new ArrayList<>(); + private boolean useTableKeyword = true; private LockMode lockMode; private boolean noWait; private Long waitSeconds; + 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()); + } +} From 443b422f3c4c7d7efc5207915ad1aabd66cd4ead Mon Sep 17 00:00:00 2001 From: minleejae Date: Mon, 21 Sep 2026 23:55:08 +0900 Subject: [PATCH 2/2] style: declare lock statement fields before nested types Signed-off-by: minleejae --- .../sf/jsqlparser/statement/lock/LockStatement.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 3b2f873d5f..ffdcbed52d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java @@ -24,6 +24,12 @@ */ public class LockStatement implements Statement { + 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 } @@ -59,12 +65,6 @@ public String toString() { } } - private final List targets = new ArrayList<>(); - private boolean useTableKeyword = true; - private LockMode lockMode; - private boolean noWait; - private Long waitSeconds; - public LockStatement() {} /**