diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index a74557bd5..5213690d1 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -84,7 +84,7 @@ jobs:
services:
postgres:
- image: postgres:11
+ image: postgres:14
env:
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
@@ -211,7 +211,7 @@ jobs:
services:
postgres:
- image: postgres:11
+ image: postgres:14
env:
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb
index df059b063..6f8df4bd7 100644
--- a/lib/arjdbc/postgresql/adapter.rb
+++ b/lib/arjdbc/postgresql/adapter.rb
@@ -22,6 +22,7 @@
require 'arjdbc/postgresql/base/array_decoder'
require 'arjdbc/postgresql/base/array_encoder'
require 'arjdbc/postgresql/name'
+require 'arjdbc/postgresql/pg_compat'
require 'arjdbc/postgresql/database_statements'
require 'arjdbc/postgresql/schema_statements'
require "arjdbc/postgresql/adapter_hash_config"
diff --git a/lib/arjdbc/postgresql/pg_compat.rb b/lib/arjdbc/postgresql/pg_compat.rb
new file mode 100644
index 000000000..a8bcd5883
--- /dev/null
+++ b/lib/arjdbc/postgresql/pg_compat.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Minimal subset of the libpq constants that ActiveRecord's native PostgreSQL
+# code paths reference as ::PG::* (e.g. #retryable_query_error? checks
+# transaction_status against PQTRANS_INERROR, and the Rails test-suite helper
+# +remote_disconnect+ uses CONNECTION_BAD / PQTRANS_INTRANS).
+#
+# The matching #status / #transaction_status / #async_exec methods are defined
+# on the JDBC connection (PostgreSQLRubyJdbcConnection) in the Java extension.
+#
+# Only defined when the real pg gem is absent (i.e. under JRuby).
+unless defined?(PG)
+ module PG
+ # PQtransactionStatus
+ PQTRANS_IDLE = 0 # connection idle, no transaction open
+ PQTRANS_ACTIVE = 1 # command in progress
+ PQTRANS_INTRANS = 2 # idle, within a transaction block
+ PQTRANS_INERROR = 3 # idle, within a failed transaction block
+ PQTRANS_UNKNOWN = 4 # connection is bad / state cannot be determined
+
+ # PQstatus (subset that AR uses)
+ CONNECTION_OK = 0
+ CONNECTION_BAD = 1
+ end
+end
diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java
index ded69a537..193846517 100644
--- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java
+++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java
@@ -214,6 +214,68 @@ public IRubyObject database_product(final ThreadContext context) {
});
}
+ // libpq PQtransactionStatus codes (see PG::PQTRANS_* / pg_compat.rb).
+ private static final int PQTRANS_IDLE = 0;
+ private static final int PQTRANS_INTRANS = 2;
+ private static final int PQTRANS_INERROR = 3;
+ private static final int PQTRANS_UNKNOWN = 4;
+ // libpq PQstatus codes (see PG::CONNECTION_* / pg_compat.rb).
+ private static final int CONNECTION_OK = 0;
+ private static final int CONNECTION_BAD = 1;
+
+ /**
+ * Mirrors PG::Connection#transaction_status. ActiveRecord's
+ * #retryable_query_error? distinguishes PQTRANS_INERROR from other states,
+ * so we read the JDBC driver's protocol-level transaction state (exposed
+ * only on the internal BaseConnection; PGConnection has no equivalent).
+ */
+ @JRubyMethod(name = "transaction_status")
+ public IRubyObject transaction_status(final ThreadContext context) {
+ // getTransactionState() is a local protocol-state read (no server
+ // round-trip), so use the plain accessor rather than withConnection's
+ // retry/reconnect machinery; a missing/closed connection or any error
+ // falls through to PQTRANS_UNKNOWN (matching libpq).
+ final Connection connection = getConnection(false);
+ try {
+ if (connection != null) {
+ final org.postgresql.core.BaseConnection base =
+ connection.unwrap(org.postgresql.core.BaseConnection.class);
+ switch (base.getTransactionState()) {
+ case IDLE: return context.runtime.newFixnum(PQTRANS_IDLE);
+ case OPEN: return context.runtime.newFixnum(PQTRANS_INTRANS);
+ case FAILED: return context.runtime.newFixnum(PQTRANS_INERROR);
+ }
+ }
+ }
+ catch (SQLException e) { /* fall through to unknown */ }
+ return context.runtime.newFixnum(PQTRANS_UNKNOWN);
+ }
+
+ /**
+ * Mirrors PG::Connection#status (CONNECTION_OK / CONNECTION_BAD).
+ * A closed connection reports CONNECTION_BAD so AR / test helpers reconnect.
+ */
+ @JRubyMethod(name = "status")
+ public IRubyObject status(final ThreadContext context) {
+ final Connection connection = getConnection(false);
+ try {
+ if (connection != null && !connection.isClosed()) {
+ return context.runtime.newFixnum(CONNECTION_OK);
+ }
+ }
+ catch (SQLException e) { /* treat as bad below */ }
+ return context.runtime.newFixnum(CONNECTION_BAD);
+ }
+
+ /**
+ * Mirrors PG::Connection#async_exec; under JDBC the statement
+ * runs synchronously. Used by Rails' +remote_disconnect+ test helper.
+ */
+ @JRubyMethod(name = "async_exec", required = 1)
+ public IRubyObject async_exec(final ThreadContext context, final IRubyObject sql) {
+ return execute(context, sql);
+ }
+
@JRubyMethod
public IRubyObject exec_params(ThreadContext context, IRubyObject sql, IRubyObject binds) {
return execute_prepared_query(context, sql, binds, null);