Skip to content

Commit a2e0cec

Browse files
authored
feat: parse PostgreSQL two-phase commit and MySQL XA statements (#2904)
PostgreSQL PREPARE TRANSACTION, COMMIT PREPARED and ROLLBACK PREPARED and the MySQL XA statements failed to parse. Add TransactionId, one identifier model for MySQL's xid (gtrid [, bqual [, formatID]]) and the single string of PostgreSQL and of Oracle's ROLLBACK FORCE. Commit and RollbackStatement name a prepared transaction through TransactionCompletionStatement, with PREPARED or FORCE as the keyword; the legacy FORCE accessors derive from it. PREPARE TRANSACTION is a new PrepareTransactionStatement, dispatched before PREPARE name AS only when a string literal follows. The XA statements are one XaStatement with an Action and an Option. The PostgreSQL forms are read with that dialect or none and take a string constant; XA is read with the MySQL and MariaDB dialects or none and follows MySQL's xid grammar. XA stays usable as a name. Wire the statements into the deparser, visitors, table finder, feature analysis and validation, with new twoPhaseCommit and xa features. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 0cb7619 commit a2e0cec

24 files changed

Lines changed: 1368 additions & 44 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
147147
| **DDL** | `CREATE …` · `ALTER …` · `DROP …` |
148148
| **Oracle vector types** | `VECTOR(*, *)` · `VECTOR(*, FLOAT32)` · `VECTOR(3, *)`, including dense and sparse storage formats |
149149
| **PostgreSQL ownership** | `DROP OWNED` · `REASSIGN OWNED` |
150+
| **Two-phase commit** | PostgreSQL `PREPARE TRANSACTION` · `COMMIT PREPARED` · `ROLLBACK PREPARED` · MySQL `XA START`/`END`/`PREPARE`/`COMMIT`/`ROLLBACK`/`RECOVER` |
150151
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
151152
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
152153
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |

‎src/main/java/net/sf/jsqlparser/parser/feature/Feature.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import net.sf.jsqlparser.statement.DeclareStatement;
2323
import net.sf.jsqlparser.statement.DescribeStatement;
2424
import net.sf.jsqlparser.statement.ExplainStatement;
25+
import net.sf.jsqlparser.statement.PrepareTransactionStatement;
2526
import net.sf.jsqlparser.statement.ResetStatement;
2627
import net.sf.jsqlparser.statement.SetConstraintsStatement;
2728
import net.sf.jsqlparser.statement.SetStatement;
@@ -30,6 +31,7 @@
3031
import net.sf.jsqlparser.statement.ShowStatement;
3132
import net.sf.jsqlparser.statement.TableMaintenanceStatement;
3233
import net.sf.jsqlparser.statement.UseStatement;
34+
import net.sf.jsqlparser.statement.XaStatement;
3335
import net.sf.jsqlparser.statement.alter.Alter;
3436
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
3537
import net.sf.jsqlparser.statement.alter.sequence.AlterSequence;
@@ -764,6 +766,18 @@ public enum Feature {
764766
* @see SetConstraintsStatement
765767
*/
766768
setConstraints,
769+
/**
770+
* PostgreSQL two-phase commit: PREPARE TRANSACTION, COMMIT PREPARED and ROLLBACK PREPARED
771+
*
772+
* @see PrepareTransactionStatement
773+
*/
774+
twoPhaseCommit,
775+
/**
776+
* MySQL and MariaDB XA statements: XA START, END, PREPARE, COMMIT, ROLLBACK and RECOVER
777+
*
778+
* @see XaStatement
779+
*/
780+
xa,
767781
/**
768782
* SQL "LOCK TABLE" statement is allowed
769783
*

‎src/main/java/net/sf/jsqlparser/statement/Commit.java‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
import java.util.Objects;
1313

14-
/** COMMIT, or its PostgreSQL spelling END. */
14+
/**
15+
* COMMIT, or its PostgreSQL spelling END. PostgreSQL {@code COMMIT PREPARED 'id'} commits a
16+
* prepared transaction instead of the current one.
17+
*/
1518
public class Commit extends TransactionCompletionStatement {
1619
/** The keyword that starts the statement; both commit the current transaction. */
1720
public enum Command {
@@ -48,6 +51,15 @@ public Commit withRelease(TransactionRelease release) {
4851
return this;
4952
}
5053

54+
/**
55+
* Names the prepared transaction of PostgreSQL {@code COMMIT PREPARED 'id'}; null commits the
56+
* current transaction.
57+
*/
58+
public Commit withPreparedTransaction(TransactionId id) {
59+
setPreparedTransaction(PreparedTransactionKeyword.PREPARED, id);
60+
return this;
61+
}
62+
5163
@Override
5264
public boolean isPostgresSpelling() {
5365
return command == Command.END;
@@ -62,6 +74,7 @@ public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
6274
public StringBuilder appendTo(StringBuilder builder) {
6375
builder.append(command);
6476
appendKeyword(builder);
77+
appendPreparedTransaction(builder);
6578
return appendCompletion(builder);
6679
}
6780
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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;
11+
12+
import java.util.Objects;
13+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
14+
15+
/**
16+
* PostgreSQL {@code PREPARE TRANSACTION 'id'}, which prepares the current transaction for two-phase
17+
* commit. {@link Commit} and {@link RollbackStatement} complete it later with
18+
* {@code COMMIT PREPARED 'id'} and {@code ROLLBACK PREPARED 'id'}. Not to be confused with
19+
* {@link PrepareStatement}, which prepares a statement.
20+
*
21+
* @see <a href="https://www.postgresql.org/docs/current/sql-prepare-transaction.html">PostgreSQL
22+
* PREPARE TRANSACTION</a>
23+
*/
24+
public class PrepareTransactionStatement extends ASTNodeAccessImpl implements Statement {
25+
private TransactionId transactionId;
26+
27+
public PrepareTransactionStatement(TransactionId transactionId) {
28+
setTransactionId(transactionId);
29+
}
30+
31+
/** The identifier that COMMIT PREPARED and ROLLBACK PREPARED name later. */
32+
public TransactionId getTransactionId() {
33+
return transactionId;
34+
}
35+
36+
public void setTransactionId(TransactionId transactionId) {
37+
this.transactionId = Objects.requireNonNull(transactionId, "transactionId");
38+
}
39+
40+
public PrepareTransactionStatement withTransactionId(TransactionId transactionId) {
41+
setTransactionId(transactionId);
42+
return this;
43+
}
44+
45+
public StringBuilder appendTo(StringBuilder builder) {
46+
return transactionId.appendTo(builder.append("PREPARE TRANSACTION "));
47+
}
48+
49+
@Override
50+
public String toString() {
51+
return appendTo(new StringBuilder()).toString();
52+
}
53+
54+
@Override
55+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
56+
return statementVisitor.visit(this, context);
57+
}
58+
}

‎src/main/java/net/sf/jsqlparser/statement/RollbackStatement.java‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,23 @@
2626
package net.sf.jsqlparser.statement;
2727

2828
import java.util.Objects;
29+
import net.sf.jsqlparser.expression.StringValue;
2930

3031
/**
31-
* ROLLBACK, or its PostgreSQL spelling ABORT.
32+
* ROLLBACK, or its PostgreSQL spelling ABORT. PostgreSQL {@code ROLLBACK PREPARED 'id'} and Oracle
33+
* {@code ROLLBACK FORCE 'id'} roll back a prepared transaction instead of the current one.
3234
*
3335
* @author are
3436
*/
3537
public class RollbackStatement extends TransactionCompletionStatement {
36-
/** The keyword that starts the statement; ABORT has no savepoint or FORCE form. */
38+
/** The keyword that starts the statement; ABORT has no savepoint, FORCE or PREPARED form. */
3739
public enum Command {
3840
ROLLBACK, ABORT
3941
}
4042

4143
private Command command = Command.ROLLBACK;
4244
private boolean usingSavepointKeyword = false;
4345
private String savepointName = null;
44-
private String forceDistributedTransactionIdentifier = null;
4546

4647
public Command getCommand() {
4748
return command;
@@ -71,6 +72,16 @@ public RollbackStatement withRelease(TransactionRelease release) {
7172
return this;
7273
}
7374

75+
/**
76+
* Names the prepared transaction of PostgreSQL {@code ROLLBACK PREPARED 'id'} or Oracle
77+
* {@code ROLLBACK FORCE 'id'}; a null identifier rolls back the current transaction.
78+
*/
79+
public RollbackStatement withPreparedTransaction(PreparedTransactionKeyword keyword,
80+
TransactionId id) {
81+
setPreparedTransaction(keyword, id);
82+
return this;
83+
}
84+
7485
public boolean isUsingWorkKeyword() {
7586
return getKeyword() == TransactionKeyword.WORK;
7687
}
@@ -114,18 +125,43 @@ public RollbackStatement withSavepointName(String savepointName) {
114125
return this;
115126
}
116127

128+
/**
129+
* The identifier of Oracle {@code ROLLBACK FORCE 'id'} as written, with its quotes, or null.
130+
* Derived from {@link #getPreparedTransactionId()} when the keyword is
131+
* {@link PreparedTransactionKeyword#FORCE}.
132+
*/
117133
public String getForceDistributedTransactionIdentifier() {
118-
return forceDistributedTransactionIdentifier;
134+
return getPreparedTransactionKeyword() == PreparedTransactionKeyword.FORCE
135+
? getPreparedTransactionId().toString()
136+
: null;
119137
}
120138

139+
/**
140+
* Sets the identifier of Oracle {@code ROLLBACK FORCE 'id'} as it is to be written. It is
141+
* stored as the {@link StringValue} of {@link #getPreparedTransactionId()}; text that is not a
142+
* string literal is kept verbatim. Null or blank removes a FORCE identifier.
143+
*/
121144
public void setForceDistributedTransactionIdentifier(
122145
String forceDistributedTransactionIdentifier) {
123-
this.forceDistributedTransactionIdentifier = forceDistributedTransactionIdentifier;
146+
if (forceDistributedTransactionIdentifier != null
147+
&& !forceDistributedTransactionIdentifier.trim().isEmpty()) {
148+
setPreparedTransaction(PreparedTransactionKeyword.FORCE,
149+
new TransactionId(asWritten(forceDistributedTransactionIdentifier)));
150+
} else if (getPreparedTransactionKeyword() == PreparedTransactionKeyword.FORCE) {
151+
setPreparedTransaction(null, null);
152+
}
153+
}
154+
155+
/** The literal {@code text} spells, or the text itself when it is not a string literal. */
156+
private static StringValue asWritten(String text) {
157+
StringValue literal = new StringValue(text);
158+
return literal.toString().equals(text) ? literal
159+
: new StringValue().withValue(text).setQuoteStr("");
124160
}
125161

126162
public RollbackStatement withForceDistributedTransactionIdentifier(
127163
String forceDistributedTransactionIdentifier) {
128-
this.forceDistributedTransactionIdentifier = forceDistributedTransactionIdentifier;
164+
setForceDistributedTransactionIdentifier(forceDistributedTransactionIdentifier);
129165
return this;
130166
}
131167

@@ -141,9 +177,8 @@ public StringBuilder appendTo(StringBuilder builder) {
141177
if (savepointName != null && !savepointName.trim().isEmpty()) {
142178
builder.append(" TO ").append(usingSavepointKeyword ? "SAVEPOINT " : "")
143179
.append(savepointName);
144-
} else if (forceDistributedTransactionIdentifier != null
145-
&& !forceDistributedTransactionIdentifier.trim().isEmpty()) {
146-
builder.append(" FORCE ").append(forceDistributedTransactionIdentifier);
180+
} else {
181+
appendPreparedTransaction(builder);
147182
}
148183
return appendCompletion(builder);
149184
}

‎src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,18 @@ public <S> Void visit(SetConstraintsStatement statement, S context) {
916916
return transactionOnly();
917917
}
918918

919+
@Override
920+
public <S> Void visit(PrepareTransactionStatement statement, S context) {
921+
return transactionOnly();
922+
}
923+
924+
/** XA RECOVER lists the prepared transactions; the other actions control a branch. */
925+
@Override
926+
public <S> Void visit(XaStatement statement, S context) {
927+
return statement.getAction() == XaStatement.Action.RECOVER ? metadataOnly()
928+
: transactionOnly();
929+
}
930+
919931
@Override
920932
public <S> Void visit(Commit commit, S context) {
921933
return transactionOnly();

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,22 @@ default void visit(SetConstraintsStatement statement) {
181181
visit(statement, null);
182182
}
183183

184+
default <S> T visit(PrepareTransactionStatement statement, S context) {
185+
return null;
186+
}
187+
188+
default void visit(PrepareTransactionStatement statement) {
189+
visit(statement, null);
190+
}
191+
192+
default <S> T visit(XaStatement statement, S context) {
193+
return null;
194+
}
195+
196+
default void visit(XaStatement statement) {
197+
visit(statement, null);
198+
}
199+
184200
<S> T visit(Commit commit, S context);
185201

186202
default void visit(Commit commit) {

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,16 @@ public <S> T visit(SetConstraintsStatement statement, S context) {
252252
return null;
253253
}
254254

255+
@Override
256+
public <S> T visit(PrepareTransactionStatement statement, S context) {
257+
return null;
258+
}
259+
260+
@Override
261+
public <S> T visit(XaStatement statement, S context) {
262+
return null;
263+
}
264+
255265
@Override
256266
public <S> T visit(Commit commit, S context) {
257267

‎src/main/java/net/sf/jsqlparser/statement/TransactionCompletionStatement.java‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,31 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12+
import java.util.Objects;
1213
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1314

1415
/**
1516
* The clauses shared by {@link Commit} and {@link RollbackStatement}:
16-
* {@code [WORK | TRANSACTION] ... [AND [NO] CHAIN] [[NO] RELEASE]}. Null fields are omitted.
17+
* {@code [WORK | TRANSACTION] ... [AND [NO] CHAIN] [[NO] RELEASE]}, or the identifier of a prepared
18+
* transaction that the statement completes instead of the current one. Null fields are omitted.
1719
*/
1820
public abstract class TransactionCompletionStatement extends ASTNodeAccessImpl
1921
implements Statement {
22+
/** The keyword before the identifier of a prepared transaction. */
23+
public enum PreparedTransactionKeyword {
24+
/** PostgreSQL {@code COMMIT PREPARED 'id'} and {@code ROLLBACK PREPARED 'id'}. */
25+
PREPARED,
26+
/**
27+
* Oracle {@code ROLLBACK [WORK] FORCE 'id'}, naming an in-doubt distributed transaction.
28+
*/
29+
FORCE
30+
}
31+
2032
private TransactionKeyword keyword;
2133
private TransactionChain chain;
2234
private TransactionRelease release;
35+
private PreparedTransactionKeyword preparedTransactionKeyword;
36+
private TransactionId preparedTransactionId;
2337

2438
public TransactionKeyword getKeyword() {
2539
return keyword;
@@ -45,6 +59,31 @@ public void setRelease(TransactionRelease release) {
4559
this.release = release;
4660
}
4761

62+
/** {@code PREPARED} or {@code FORCE} when a prepared transaction is named, otherwise null. */
63+
public PreparedTransactionKeyword getPreparedTransactionKeyword() {
64+
return preparedTransactionKeyword;
65+
}
66+
67+
/**
68+
* The identifier of the prepared transaction to complete, written after
69+
* {@link #getPreparedTransactionKeyword()}, or null when the current transaction completes.
70+
*/
71+
public TransactionId getPreparedTransactionId() {
72+
return preparedTransactionId;
73+
}
74+
75+
/**
76+
* Names the prepared transaction to complete. A null identifier completes the current
77+
* transaction and clears the keyword.
78+
*
79+
* @throws NullPointerException if an identifier is given without a keyword
80+
*/
81+
public void setPreparedTransaction(PreparedTransactionKeyword keyword, TransactionId id) {
82+
this.preparedTransactionKeyword =
83+
id == null ? null : Objects.requireNonNull(keyword, "keyword");
84+
this.preparedTransactionId = id;
85+
}
86+
4887
/** Whether the statement is spelled END or ABORT, PostgreSQL's names of COMMIT and ROLLBACK. */
4988
public abstract boolean isPostgresSpelling();
5089

@@ -55,6 +94,15 @@ protected StringBuilder appendKeyword(StringBuilder builder) {
5594
return keyword == null ? builder : builder.append(' ').append(keyword);
5695
}
5796

97+
/** Appends {@code PREPARED 'id'} or {@code FORCE 'id'} when a prepared transaction is named. */
98+
protected StringBuilder appendPreparedTransaction(StringBuilder builder) {
99+
if (preparedTransactionId != null) {
100+
builder.append(' ').append(preparedTransactionKeyword).append(' ');
101+
preparedTransactionId.appendTo(builder);
102+
}
103+
return builder;
104+
}
105+
58106
/** Appends {@code AND [NO] CHAIN} and {@code [NO] RELEASE}, which end the statement. */
59107
protected StringBuilder appendCompletion(StringBuilder builder) {
60108
if (chain != null) {

0 commit comments

Comments
 (0)