Skip to content

feat: parse PostgreSQL two-phase commit and MySQL XA statements - #2904

Merged
manticore-projects merged 1 commit into
JSQLParser:masterfrom
minleejae:feat/two-phase-transactions-20261107
Nov 9, 2026
Merged

manticore-projects merged 1 commit into
JSQLParser:masterfrom
minleejae:feat/two-phase-transactions-20261107

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

PostgreSQL's two-phase commit statements and MySQL's XA statements failed to parse:

-- PostgreSQL
PREPARE TRANSACTION 'foobar';
COMMIT PREPARED 'foobar';
ROLLBACK PREPARED 'foobar';

-- MySQL / MariaDB
XA START 'x', 'b', 1;
XA END 'x' SUSPEND FOR MIGRATE;
XA PREPARE 'x';
XA COMMIT 'x' ONE PHASE;
XA RECOVER CONVERT XID;

PREPARE TRANSACTION 'foobar' failed at the string literal because PREPARE always read
PREPARE name AS statement; COMMIT PREPARED and ROLLBACK PREPARED failed at PREPARED;
and XA was not a statement start.

Current design (As-is)

  • Commit and RollbackStatement share TransactionCompletionStatement (feat: parse SET TRANSACTION, SET CONSTRAINTS, COMMIT RELEASE and PostgreSQL END and ABORT #2889) for
    [WORK | TRANSACTION], AND [NO] CHAIN and [NO] RELEASE; neither can name a prepared
    transaction.
  • Oracle's ROLLBACK [WORK] FORCE 'id' is the only statement that names one. It is kept as the
    raw token text in RollbackStatement.forceDistributedTransactionIdentifier, a String.
  • PREPARE always starts a PrepareStatement.
  • There is no XA model.

Target design (To-be)

  • One transaction identifier model. TransactionId holds a global transaction identifier
    plus MySQL's optional branch qualifier and format ID (gtrid [, bqual [, formatID]]). The
    identifier parts are the literals as written (StringValue, or in MySQL also HexValue and
    bit values). PostgreSQL's identifier, MySQL's xid and Oracle's FORCE identifier all use it.
  • COMMIT / ROLLBACK PREPARED. TransactionCompletionStatement gains
    getPreparedTransactionKeyword() (PREPARED or FORCE) and getPreparedTransactionId().
    COMMIT PREPARED 'id' and ROLLBACK PREPARED 'id' produce the existing Commit and
    RollbackStatement, so consumers keep one model per completion. Oracle's ROLLBACK FORCE 'id'
    moves to the same pair. getForceDistributedTransactionIdentifier() and its setter remain,
    derived from that pair, and return and render the identifier exactly as before.
  • PREPARE TRANSACTION. PrepareTransactionStatement is dispatched before
    PREPARE name AS statement, only when a string literal follows TRANSACTION. So
    PREPARE transaction AS SELECT 1 still prepares a statement named transaction.
  • XA. XaStatement has an Action (START, BEGIN, END, PREPARE, COMMIT,
    ROLLBACK, RECOVER), the xid as a TransactionId (null for RECOVER), and an optional
    Option (JOIN, RESUME, SUSPEND, SUSPEND FOR MIGRATE, ONE PHASE, CONVERT XID).
    The model rejects an option that does not belong to the action.
  • Dialects.
    • The PostgreSQL forms are read with the PostgreSQL dialect or without one. Their identifier
      must be a PostgreSQL string constant ('…', E'…', U&'…', dollar-quoted), not N'…',
      B'…' or X'…'. The other spellings of PREPARED (COMMIT WORK PREPARED, END/ABORT,
      AND CHAIN) are rejected.
    • The XA forms are read with the MySQL and MariaDB dialects or without one. XA is not a
      token, so it stays usable as a name.
    • Following MySQL's xid rule, gtrid and bqual are string, hexadecimal (X'..' with whole
      bytes, 0x..) or bit-value literals. N'…', _charset'…', numbers and adjacent strings are
      rejected. formatID is an unsigned integer up to LONG_MAX (MariaDB: INT32_MAX), the limits
      of each server's grammar.
    • MariaDB rejects CONVERT XID.
    • Other dialects are unchanged.
  • Wiring. Statement visitors, StatementDeParser (through the shared appendTo),
    TablesNamesFinder (no tables), and feature analysis (XA RECOVER returns a result set; the
    rest is transaction control) all handle the new statements. Validation gets two new features:
    twoPhaseCommit in the PostgreSQL capabilities and xa in the MySQL and MariaDB capabilities.

References: PostgreSQL PREPARE TRANSACTION,
COMMIT PREPARED,
ROLLBACK PREPARED,
MySQL 8.4 XA statements.

Not included:

  • MariaDB's XA RECOVER FORMAT = 'SQL'.
  • MySQL formatIDs written as hexadecimal, decimal or float literals, which the server truncates
    to an integer.
  • 0b… bit values.
  • PostgreSQL string constants continued on a new line.
  • The 64-byte limits on gtrid and bqual, which depend on the connection character set.

Validation

  • New TwoPhaseTransactionTest with 126 test cases. They cover parsing with and without each
    dialect, toString, StatementDeParser, reparse, AST fields, node spans, rejected forms,
    other dialects keeping their readings (PREPARE transaction AS …, XA as a name), statement
    boundaries with and without semicolons (including opaque-statement mode), visitors,
    TablesNamesFinder, feature analysis, validation and the model API.

  • Updated tests:

    • ASTNodeCoverageTest walks seven of the new forms.
    • StatementValidatorTest reports twoPhaseCommit and xa.
    • TransactionStatementTest no longer lists COMMIT PREPARED 'id' and
      ROLLBACK PREPARED 'id' as rejected PostgreSQL syntax.
  • These tests fail on master, where none of the forms parse.

  • ./gradlew spotlessApply check jar: 13054 tests, 25 skipped, 0 failures. Grammar ambiguity
    check, Checkstyle, PMD, SpotBugs and JaCoCo pass.

  • Compared with master (a30a67920) on the non-DDL survey corpora, 12 cases are fixed: XA START, XA END and XA COMMIT … ONE PHASE in the MySQL dialect and without one, and PREPARE TRANSACTION, COMMIT PREPARED and ROLLBACK PREPARED in the PostgreSQL dialect and without
    one. Nothing regressed or changed there, and the DDL/DCL survey corpora have 0 regressions.

  • PostgreSQL 18.6 accepts all 24 statements the tests accept for the PostgreSQL dialect, 14 of which master rejects. Some fail only when run, for example prepared transaction with identifier "x" does not exist or SAVEPOINT can only be used in transaction blocks. It reports a syntax error for all 31 statements the tests reject, including COMMIT PREPARED 'x' AND CHAIN, END PREPARED 'x' and the XA forms.

  • MySQL 8.4.11 accepts all 33 statements the tests accept for the MySQL dialect, 27 of which master rejects. Some fail only when run with an XA state error (XAER_NOTA, XAER_INVAL, XAER_RMFAIL). It reports a syntax error (1064) for all 51 statements the tests reject, including XA START 'a', 'b', -1, XA START 'a', 'b', 9223372036854775808, XA START N'x', XA END 'x' FOR MIGRATE and the PostgreSQL forms.

AI assistance: prepared with Claude Code.

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>
@manticore-projects
manticore-projects merged commit a2e0cec into JSQLParser:master Nov 9, 2026
10 checks passed
@manticore-projects

Copy link
Copy Markdown
Contributor

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants