You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Rails 8.0+ transaction materialization breaks other tests #1212
While working to finalize Rails 8.0 support I discovered a number of tests failing depending on the ordering of the test suite. I eventually narrowed the problems down to a set of tests for "transaction materialization" in the Rails 8.0 suite here:
Several of the tests in this block use @connection.materialize_transactions to preserve a transaction across a reconnect. Either this logic or the associated setup/teardown of the spec causes the adapter to leave a transaction open on JRuby's adapter (tested on sqlite), usually causing many subsequent tests to fail until the system resets enough to return to a stable state.
The number of failures can reach into the thousands depending on when this block of tests runs.
If I comment out this entire block, the test suite is much more stable, and the total number of failures is in the hundreds, roughly 3-5% of all tests run.
I think I've found the root cause. It's not the materialize_transactions logic itself — it's Rails' raw_transaction_open? test helper colliding with a legacy method alias on the JDBC raw connection.
The collision
The helper probes for an open transaction on SQLite like this (adapter_test.rb):
On MRI, SQLite3::Database#transaction with a block is BEGIN → yield → COMMIT — a net-zero probe that raises cannot start a transaction within a transaction when one is already open. Neat.
Under AR-JDBC, @raw_connection is a SQLite3JdbcConnection, and its transaction method is something else entirely: a legacy alias of begin dating back to AR 4.0 support (RubyJdbcConnection.java#L335-L344). Since the Java method doesn't take a block, the block is silently ignored. So the probe:
with no transaction open: calls setAutoCommit(false) — opening a real transaction that nothing ever commits — and returns the "right" answer while leaving a zombie transaction behind;
with a transaction open: hits if (getAutoCommit()) → no-op, no exception, so the helper reports "no transaction" (this is why the assertions in that block also fail directly).
Reproduction (72-stable, AR 7.2.3.1, jruby-9.4.14.0 — the alias and the helper are identical on master / Rails 8):
raw=ActiveRecord::Base.connection.instance_variable_get(:@raw_connection)java=raw.jdbc_connection(true)java.auto_commit# => trueraw.transaction{:ran}# => nil (block ignored; MRI returns the block value)java.auto_commit# => false (zombie transaction is now open)raw.transaction{nil}# => nil (no exception; MRI raises "cannot start a transaction within a transaction")# everything that follows silently lands inside the zombie tx — DDL included:conn.execute("CREATE TABLE poison_check (id integer)")conn.execute("INSERT INTO poison_check (id) VALUES (1)")raw.rollbackconn.select_value("SELECT COUNT(*) FROM poison_check")# => SQLiteException: no such table: poison_check (the rollback erased the CREATE TABLE)
That last part is the cascade mechanism: after the probe runs, every subsequent statement on that pooled connection — fixture loads, schema changes, the transactional-test wrapper — runs inside a transaction Rails doesn't know about, and the next stray rollback/commit lands in the wrong place. Hence order-dependent failures in the thousands, and why disabling the whole AdapterConnectionTest block stabilizes the suite.
Proposed fix
Give the SQLite JDBC connection a real transaction method with SQLite3::Database semantics (block: begin → yield → commit, rollback + re-raise on error; raise when already in a transaction), shadowing the legacy alias — same shim approach as #1220 took for the PG branch of this helper (transaction_status / PG::PQTRANS_*). Removing the "transaction" alias from the base class outright would also work but risks breaking external callers, so I'd keep that separate.
Happy to prepare a PR if this direction sounds right.
Activity
headius commentedon May 28, 2026
My way of disabling the test:
headius commentedon May 28, 2026
The MySQL tests run even better than the SQLite tests with this block disabled:
ryudoawaru commentedon Jul 27, 2026
I think I've found the root cause. It's not the
materialize_transactionslogic itself — it's Rails'raw_transaction_open?test helper colliding with a legacy method alias on the JDBC raw connection.The collision
The helper probes for an open transaction on SQLite like this (adapter_test.rb):
On MRI,
SQLite3::Database#transactionwith a block isBEGIN→ yield →COMMIT— a net-zero probe that raisescannot start a transaction within a transactionwhen one is already open. Neat.Under AR-JDBC,
@raw_connectionis aSQLite3JdbcConnection, and itstransactionmethod is something else entirely: a legacy alias ofbegindating back to AR 4.0 support (RubyJdbcConnection.java#L335-L344). Since the Java method doesn't take a block, the block is silently ignored. So the probe:setAutoCommit(false)— opening a real transaction that nothing ever commits — and returns the "right" answer while leaving a zombie transaction behind;if (getAutoCommit())→ no-op, no exception, so the helper reports "no transaction" (this is why the assertions in that block also fail directly).Reproduction (72-stable, AR 7.2.3.1, jruby-9.4.14.0 — the alias and the helper are identical on master / Rails 8):
That last part is the cascade mechanism: after the probe runs, every subsequent statement on that pooled connection — fixture loads, schema changes, the transactional-test wrapper — runs inside a transaction Rails doesn't know about, and the next stray rollback/commit lands in the wrong place. Hence order-dependent failures in the thousands, and why disabling the whole
AdapterConnectionTestblock stabilizes the suite.Proposed fix
Give the SQLite JDBC connection a real
transactionmethod withSQLite3::Databasesemantics (block: begin → yield → commit, rollback + re-raise on error; raise when already in a transaction), shadowing the legacy alias — same shim approach as #1220 took for the PG branch of this helper (transaction_status/PG::PQTRANS_*). Removing the"transaction"alias from the base class outright would also work but risks breaking external callers, so I'd keep that separate.Happy to prepare a PR if this direction sounds right.
headius commentedon Aug 6, 2026
@ryudoawaru Note there were some related fixes for transaction management in #1218 from @skunkworker.
I see what you see and yes, the
transactionmethod should be split off and handle a passed block appropriately. Go ahead and make a PR for that.