Skip to content

feat: parse SET TRANSACTION, SET CONSTRAINTS, COMMIT RELEASE and PostgreSQL END and ABORT - #2889

Open
minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/transaction-control-20261103
Open

minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/transaction-control-20261103

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

Applications and replication tools send transaction control statements that JSqlParser cannot parse today:

  • MySQL SET TRANSACTION ISOLATION LEVEL READ COMMITTED, SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY, SET GLOBAL TRANSACTION READ ONLY, SET LOCAL TRANSACTION READ ONLY, SET PERSIST TRANSACTION READ ONLY and SET PERSIST_ONLY TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • PostgreSQL SET TRANSACTION ISOLATION LEVEL …, SET LOCAL TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY, SET TRANSACTION SNAPSHOT '00000003-0000001B-1', SET SESSION TRANSACTION SNAPSHOT '…', SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE and SET LOCAL SESSION CHARACTERISTICS AS TRANSACTION READ ONLY;
  • MySQL COMMIT AND NO CHAIN NO RELEASE, COMMIT RELEASE and ROLLBACK WORK AND CHAIN NO RELEASE;
  • PostgreSQL END and ABORT (END WORK AND CHAIN, ABORT TRANSACTION);
  • PostgreSQL SET CONSTRAINTS ALL DEFERRED.

SET CONSTRAINTS c1, c2 IMMEDIATE parses, but as the two variable assignments CONSTRAINTS = c1 and c2 = IMMEDIATE.

References: MySQL 8.4 SET TRANSACTION, SET for variable assignment (its scope keywords) and START TRANSACTION, COMMIT and ROLLBACK; MariaDB SET TRANSACTION; PostgreSQL 18 SET TRANSACTION, SET (SESSION and LOCAL), SET CONSTRAINTS, END and ABORT.

The pages document fewer scopes than the servers accept. The MySQL 8.4 grammar takes every scope of SET assignments before TRANSACTION (GLOBAL, SESSION, LOCAL, PERSIST, PERSIST_ONLY), MariaDB's takes GLOBAL, SESSION and LOCAL, and PostgreSQL's SET [SESSION | LOCAL] prefix applies to TRANSACTION, TRANSACTION SNAPSHOT and SESSION CHARACTERISTICS AS TRANSACTION alike. The parser follows the grammars.

Current design (As-is)

  • Transaction modes: StartTransaction.Mode (Support typed transaction starts, chaining and savepoint release #2729) holds the characteristics of BEGIN and START TRANSACTION.
    • TransactionMode() raises errors that only hold for START TRANSACTION, such as "MySQL START TRANSACTION does not accept ISOLATION LEVEL"; in MySQL SET TRANSACTION the isolation level is the main clause.
    • The comma rule and the MySQL READ ONLY + READ WRITE check sit inline in StartTransaction(), so another statement cannot reuse the list.
    • StatementValidator maps the modes to features in a switch that only StartTransaction can use.
  • SET: every SET that is not a SQL Server option or a MySQL SET ROLE, SET DEFAULT ROLE or SET PASSWORD goes to SetStatement.
    • SET TRANSACTION … fails, because TRANSACTION is read as a variable name.
    • SetStatement.effectParameter is a raw String (LOCAL or SESSION as written), and SetStatement.NameExpr is package-private although the public getKeyValuePairs() returns it.
  • COMMIT and ROLLBACK: Commit and RollbackStatement each hold TransactionKeyword and TransactionChain and render them with their own code. There is no [NO] RELEASE, and no spelling other than COMMIT and ROLLBACK.
  • END closes BEGIN … END blocks and CASE expressions; it never starts a statement. ABORT is not a statement.
  • Validation: RollbackStatement checks only transactionChain, and SavepointStatement checks nothing (a TODO).

Target design (To-be)

  • TransactionMode: a top-level enum replaces StartTransaction.Mode, with isIsolationLevel(), isAccessMode() and isDeferrability(). StartTransaction.Mode came with Support typed transaction starts, chaining and savepoint release #2729 (merged 2026-09-28); the latest release, 5.4, is from 2026-09-13 and nothing has been released since, so replacing it does not break a released API.
    • One TransactionModeList(modes, context) production parses the list for BEGIN, START TRANSACTION and SET TRANSACTION (TransactionMode.Context). TransactionMode() itself has no dialect checks.
    • One helper applies the dialect rules of the context: MySQL BEGIN takes no modes, MySQL START TRANSACTION no isolation level and not both access modes, MySQL SET TRANSACTION one isolation level and one access mode, MySQL never DEFERRABLE; only START TRANSACTION takes WITH CONSISTENT SNAPSHOT, and PostgreSQL never.
    • The validator maps the modes with one shared helper. For SET TRANSACTION the isolation level is not a separate feature.
  • SetTransactionStatement for SET [scope] TRANSACTION modes, SET [scope] SESSION CHARACTERISTICS AS TRANSACTION modes and SET [scope] TRANSACTION SNAPSHOT 'id':
    • getScope() returns the scope keyword written after SET as the new SetScope enum (NONE, GLOBAL, SESSION, LOCAL, PERSIST, PERSIST_ONLY), shared with SetStatement. isSessionCharacteristics() marks the PostgreSQL form independently of the scope, so SET SESSION CHARACTERISTICS … (scope NONE), SET LOCAL SESSION CHARACTERISTICS … and SET SESSION SESSION CHARACTERISTICS … each print back as written.

    • getModes() is the TransactionMode list, and getSnapshotId() a StringValue.

    • Dialect rules, following each server's grammar:

      Form MySQL MariaDB PostgreSQL No dialect
      SET [scope] TRANSACTION modes none, GLOBAL, SESSION, LOCAL, PERSIST, PERSIST_ONLY none, GLOBAL, SESSION, LOCAL none, SESSION, LOCAL any of these
      SET [scope] TRANSACTION SNAPSHOT 'id' rejected rejected none, SESSION, LOCAL none, SESSION, LOCAL
      SET [scope] SESSION CHARACTERISTICS AS TRANSACTION modes rejected rejected none, SESSION, LOCAL none, SESSION, LOCAL

      SESSION CHARACTERISTICS never takes SNAPSHOT. MySQL and MariaDB also require commas and at most one isolation level and one access mode.

  • SetConstraintsStatement for SET CONSTRAINTS {ALL | name [, …]} {DEFERRED | IMMEDIATE}:
    • getConstraintNames() keeps each name as written (qualified, quoted); an empty list means ALL (isAll()).
    • getTiming() reuses ConstraintAttributes.Initially, the enum of INITIALLY DEFERRED | IMMEDIATE.
    • The MySQL and MariaDB dialects reject it.
  • Dispatch: both statements are routed in SingleStatement before Set(), after the MySQL SET ROLE, SET DEFAULT ROLE and SET PASSWORD checks, by token checks. SET [scope] TRANSACTION needs a mode or SNAPSHOT 'id' after it, and SET CONSTRAINTS no =, := or TO, so assignments such as SET transaction_isolation = 'READ-COMMITTED', SET PERSIST transaction_isolation = 'READ-COMMITTED', SET LOCAL transaction = 1, SET SESSION work_mem TO DEFAULT, SET @@SESSION.sql_mode = … and SET LOCAL statement_timeout = '5s' remain SetStatement.
  • SetStatement: getScope()/setScope() type the scope. getEffectParameter() and setEffectParameter(String) remain, derived from the scope. NameExpr is public, with getAssignmentOperator()/setAssignmentOperator(). TablesNamesFinder keeps finding tables in SET values (fix: find tables in SET values instead of throwing #2873).
  • COMMIT and ROLLBACK: both extend the new TransactionCompletionStatement, which holds keyword, chain and the new TransactionRelease (RELEASE, NO_RELEASE) and renders them once.
    • TransactionCompletionStatement extends ASTNodeAccessImpl, so Commit and RollbackStatement keep the AST node fix: give every parsed object a node spanning its source #2870 gave them.
    • Commit.Command (COMMIT, END) and RollbackStatement.Command (ROLLBACK, ABORT) record the spelling, mirroring StartTransaction.Command, so consumers see one model per completion; isPostgresSpelling() reports END and ABORT.
    • [NO] RELEASE follows the chain; AND CHAIN RELEASE, RELEASE after TO SAVEPOINT, and RELEASE in PostgreSQL are rejected. RELEASE SAVEPOINT sp and RELEASE sp remain separate statements.
  • END and ABORT:
    • END [WORK | TRANSACTION] [AND [NO] CHAIN] is a statement only with the PostgreSQL dialect, at the start of a statement, like isBeginTransactionAhead. The statement list of a BEGIN … END block stops at END first, so blocks, CASE … END, IF … BEGIN … END ELSE and BEGIN ATOMIC … END bodies keep their reading.
    • ABORT [WORK | TRANSACTION] [AND [NO] CHAIN] is read with the PostgreSQL dialect and without a dialect. It is not a token, so abort stays an identifier; a JAVACODE production consumes it, so identifiers do not join the tokens that can start or end a statement.
    • isStatementStartAhead lists both, so they end a preceding statement and are not captured as unsupported statements.
  • AST nodes (fix: give every parsed object a node spanning its source #2870): SetTransactionStatement and SetConstraintsStatement extend ASTNodeAccessImpl. Like every statement, they get the node SingleStatement links, from the first token to the last; the snapshot StringValue gets the node of its literal.
  • Grammar: no new tokens or reserved words; the grammar choice conflict check passes.
  • Validation:
    • New features rollback, savepoint, setTransaction, setTransactionGlobal, setTransactionPersist (PERSIST and PERSIST_ONLY), setTransactionSessionCharacteristics, setTransactionSnapshot, setConstraints, transactionRelease and transactionEndAbort. LOCAL and SESSION need no feature, since MySQL and PostgreSQL both accept them.
    • ROLLBACK checks rollback, and SAVEPOINT and ROLLBACK TO check savepoint. Both are added to every capability set that has commit (savepoint not for SQL Server, which uses SAVE TRANSACTION).
    • MySQL 8.0 adds setTransaction, setTransactionGlobal, setTransactionPersist and transactionRelease; PostgreSQL 10 adds setTransaction, setTransactionSessionCharacteristics, setTransactionSnapshot, setConstraints and transactionEndAbort.
  • Wired into StatementVisitor (default methods), StatementVisitorAdapter, StatementDeParser, TablesNamesFinder (no tables), StatementFeatureVisitor (MODIFIES_TRANSACTION) and usage.rst.
  • Compatibility:
    • StartTransaction.getModes() now returns List<TransactionMode>; StartTransaction.Mode is gone. It was added by Support typed transaction starts, chaining and savepoint release #2729 after the 5.4 release and is in no release.
    • SetStatement renders the scope keyword in upper case (set local x = 1 becomes SET LOCAL x = 1), and setEffectParameter rejects a word that is not a SetScope with IllegalArgumentException.
    • Commit and RollbackStatement keep their getters, setters and fluent methods; the shared ones moved to the superclass, which also carries their AST node.
    • SET CONSTRAINTS c1, c2 IMMEDIATE is now a SetConstraintsStatement, not a SetStatement, in every dialect but MySQL and MariaDB, where it is rejected.
    • The H2Version capability list is reformatted by spotlessApply because the file is touched; only rollback and savepoint are added.
  • usage.rst documents the statements, the shared models and the validator features.

Validation

  • 136 new tests:
    • TransactionStatementTest (54 new):
      • MySQL/MariaDB [NO] RELEASE with and without AND [NO] CHAIN, AST fields, and rejected AND CHAIN RELEASE, RELEASE after TO SAVEPOINT and PostgreSQL RELEASE;
      • PostgreSQL END/ABORT with WORK/TRANSACTION and chains, ABORT without a dialect, rejection in MySQL, MariaDB, Oracle and SQL Server, and END TO SAVEPOINT, ABORT TO sp and END garbage;
      • BEGIN … END blocks with every dialect, CASE … END, IF … BEGIN … END ELSE, BEGIN ATOMIC … END, and abort/end as identifiers;
      • a parameterized test in which each new statement start (END, ABORT, COMMIT … RELEASE, ROLLBACK RELEASE, the SET … TRANSACTION forms with and without a scope, SET CONSTRAINTS) ends a preceding COMMIT without a semicolon, with and without unsupported-statement capture, and both statements' AST nodes span exactly their own text;
      • the shared completion model, visitors, and validation of rollback, savepoint, transactionRelease and transactionEndAbort.
    • SetTransactionStatementTest (67): the MySQL forms with Dialect.MYSQL, Dialect.MARIADB and no dialect; every scope in every dialect (LOCAL, SESSION, GLOBAL, PERSIST, PERSIST_ONLY), accepted or rejected as in the table above; PostgreSQL characteristics with spaces or commas, SESSION CHARACTERISTICS with no scope, SESSION or LOCAL, and SNAPSHOT with no scope, SESSION or LOCAL; combinations that no server accepts (SET GLOBAL TRANSACTION SNAPSHOT 'x', SET PERSIST SESSION CHARACTERISTICS AS TRANSACTION READ ONLY, SESSION CHARACTERISTICS … SNAPSHOT); parse, AST node range (the statement and the snapshot literal), toString, deparser, reparse, AST fields, feature analysis and table finder; MySQL- and PostgreSQL-only forms rejected in the other dialect; malformed forms; variable assignments that stay SetStatement, including SET PERSIST transaction_isolation = …, SET LOCAL transaction = 1 and the other scopes; edits; visitors, boundaries and validation.
    • SetConstraintsStatementTest (13): ALL and named, qualified and quoted constraints with PostgreSQL, Oracle and no dialect, each with its AST node range; the former assignment reading; rejected forms and MySQL; edits; visitors, boundaries and validation.
    • SetStatementTest (2): the typed scope behind getEffectParameter()/setEffectParameter(), and the public NameExpr.
    • The round-trip helper of TransactionStatementTest also checks that every transaction statement it parses (including the existing BEGIN, START TRANSACTION, SAVEPOINT and RELEASE forms) has a node spanning its whole text.
  • Java 17 offline Gradle spotlessApply cleanTest check jar: 12,568 tests, 12,543 passed, 25 skipped, zero failures/errors. ASTNodeCoverageTest passes; a one-off run of its coverage walk over scripts of every new form, in the PostgreSQL, MySQL and default dialects, reported no problem. The grammar choice conflict check, Checkstyle, PMD, SpotBugs and JaCoCo pass.
  • The SET … TRANSACTION, SET CONSTRAINTS, COMMIT/ROLLBACK … [NO] RELEASE, END and ABORT forms were run against MySQL 8.4.11 and PostgreSQL 18: every form the parser accepts in a dialect is accepted by that server's grammar, and every form it rejects in a dialect is a syntax error on that server. The one exception is SET TRANSACTION SNAPSHOT without an identifier, which PostgreSQL rejects but which still parses as a SetStatement assignment, as on master. The MariaDB rules follow MariaDB's grammar (option_type is GLOBAL | LOCAL | SESSION).
  • Compared with master (7123d25a1) on a corpus of MySQL 8.4 and PostgreSQL 18 non-DDL statements, each accepted by the server:
    • Fixed with Dialect.MYSQL and without a dialect: COMMIT AND NO CHAIN NO RELEASE, SET TRANSACTION ISOLATION LEVEL READ COMMITTED, SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY and SET GLOBAL TRANSACTION READ WRITE.
    • Fixed with Dialect.POSTGRESQL: END, ABORT, SET TRANSACTION ISOLATION LEVEL READ COMMITTED, SET TRANSACTION SNAPSHOT '00000003-0000001B-1', SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY and SET CONSTRAINTS ALL DEFERRED; the same without a dialect except END, which stays rejected there.
    • SET CONSTRAINTS c1, c2 IMMEDIATE is now a SetConstraintsStatement instead of two assignments, with Dialect.POSTGRESQL and without a dialect.
    • 0 regressions; no other case changed status or statement class.
    • Outside the corpus, SET GLOBAL TRANSACTION READ ONLY, SET LOCAL TRANSACTION READ ONLY, SET PERSIST TRANSACTION READ ONLY and SET PERSIST_ONLY TRANSACTION ISOLATION LEVEL READ COMMITTED (MySQL), SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE, SET LOCAL SESSION CHARACTERISTICS AS TRANSACTION READ ONLY and SET SESSION TRANSACTION SNAPSHOT '…' (PostgreSQL), ROLLBACK WORK AND CHAIN NO RELEASE (MySQL) and COMMIT RELEASE (MariaDB) fail on master and parse here; PREPARE TRANSACTION, COMMIT PREPARED and XA still fail, as before.
  • On the DDL/DCL corpus of MySQL 8.4 and PostgreSQL 18 statements plus variants, keyword and script corpora, no case changed.
  • Out of scope: PostgreSQL PREPARE TRANSACTION, COMMIT PREPARED / ROLLBACK PREPARED and MySQL XA statements.

AI assistance: prepared with Claude Code.

…greSQL END and ABORT

MySQL SET [scope] TRANSACTION, PostgreSQL SET [scope] TRANSACTION,
SET TRANSACTION SNAPSHOT, SET SESSION CHARACTERISTICS AS TRANSACTION,
SET CONSTRAINTS, MySQL COMMIT/ROLLBACK ... [NO] RELEASE and PostgreSQL
END and ABORT failed to parse; SET CONSTRAINTS c1, c2 IMMEDIATE was
read as two variable assignments.

Move the transaction characteristics to a top-level TransactionMode
enum, parsed by one TransactionModeList production whose context
(BEGIN, START TRANSACTION, SET TRANSACTION) selects the dialect rules.
Add SetTransactionStatement and SetConstraintsStatement, dispatched
before the generic SET, which keeps variable assignments. They reuse
TransactionMode, a new SetScope enum that also types the scope of
SetStatement, and ConstraintAttributes.Initially. SET TRANSACTION takes
the scopes of each server's grammar: GLOBAL, SESSION, LOCAL, PERSIST
and PERSIST_ONLY in MySQL, the first three in MariaDB, and SESSION and
LOCAL in PostgreSQL, where they also precede SNAPSHOT and SESSION
CHARACTERISTICS.

Commit and RollbackStatement share TransactionCompletionStatement for
the keyword, chain and the new TransactionRelease, and record the END
and ABORT spellings in Command enums. END is a statement only with
the PostgreSQL dialect and outside BEGIN ... END blocks.

The new statements and TransactionCompletionStatement extend
ASTNodeAccessImpl, so each parsed statement keeps the node spanning
its source.

Wire the statements into the deparser, visitors, table finder,
feature analysis and validation, adding the rollback and savepoint
features and the MySQL and PostgreSQL capabilities.

Signed-off-by: minleejae <mmj9808@gmail.com>

This branch has not been deployed

No deployments
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.

1 participant