Skip to content

Commit 8de260a

Browse files
committed
feat: parse PostgreSQL two-phase commit and MySQL XA statements
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 a30a679 commit 8de260a

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
@@ -915,6 +915,18 @@ public <S> Void visit(SetConstraintsStatement statement, S context) {
915915
return transactionOnly();
916916
}
917917

918+
@Override
919+
public <S> Void visit(PrepareTransactionStatement statement, S context) {
920+
return transactionOnly();
921+
}
922+
923+
/** XA RECOVER lists the prepared transactions; the other actions control a branch. */
924+
@Override
925+
public <S> Void visit(XaStatement statement, S context) {
926+
return statement.getAction() == XaStatement.Action.RECOVER ? metadataOnly()
927+
: transactionOnly();
928+
}
929+
918930
@Override
919931
public <S> Void visit(Commit commit, S context) {
920932
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
@@ -180,6 +180,22 @@ default void visit(SetConstraintsStatement statement) {
180180
visit(statement, null);
181181
}
182182

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

185201
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
@@ -250,6 +250,16 @@ public <S> T visit(SetConstraintsStatement statement, S context) {
250250
return null;
251251
}
252252

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

‎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)