From 64021ef9f3168b34adb79c75cc36404ce6c032b6 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Tue, 30 Jun 2026 13:52:37 +1000 Subject: [PATCH 01/36] Revert "Add Rails 8.1 compatibility" This reverts commit 22812411fa6956f66858825bc3fb6730dd9e7b0d. Signed-off-by: Jesse Chavez --- activerecord-jdbc-adapter.gemspec | 2 +- lib/arjdbc/abstract/database_statements.rb | 15 --------------- lib/arjdbc/postgresql/schema_statements.rb | 17 ----------------- 3 files changed, 1 insertion(+), 33 deletions(-) diff --git a/activerecord-jdbc-adapter.gemspec b/activerecord-jdbc-adapter.gemspec index f12115079..a32506441 100644 --- a/activerecord-jdbc-adapter.gemspec +++ b/activerecord-jdbc-adapter.gemspec @@ -41,7 +41,7 @@ Gem::Specification.new do |gem| gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^test/}) - gem.add_dependency "activerecord", "~> 8.0" + gem.add_dependency "activerecord", "~> 8.0.0" #gem.add_development_dependency 'test-unit', '2.5.4' #gem.add_development_dependency 'test-unit-context', '>= 0.3.0' diff --git a/lib/arjdbc/abstract/database_statements.rb b/lib/arjdbc/abstract/database_statements.rb index 856619a23..64a6a82f6 100644 --- a/lib/arjdbc/abstract/database_statements.rb +++ b/lib/arjdbc/abstract/database_statements.rb @@ -9,21 +9,6 @@ module DatabaseStatements NO_BINDS = [].freeze - unless method_defined?(:mark_transaction_written_if_write) - def mark_transaction_written_if_write(sql) - if write_query?(sql) - ensure_writes_are_allowed(sql) - mark_transaction_written - end - end - end - - unless method_defined?(:check_if_write_query) - def check_if_write_query(sql) - ensure_writes_are_allowed(sql) if write_query?(sql) - end - end - def exec_insert(sql, name = nil, binds = NO_BINDS, pk = nil, sequence_name = nil, returning: nil) if preventing_writes? raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" diff --git a/lib/arjdbc/postgresql/schema_statements.rb b/lib/arjdbc/postgresql/schema_statements.rb index 50d64bfea..41f3ef91b 100644 --- a/lib/arjdbc/postgresql/schema_statements.rb +++ b/lib/arjdbc/postgresql/schema_statements.rb @@ -6,23 +6,6 @@ module SchemaStatements ForeignKeyDefinition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition Utils = ActiveRecord::ConnectionAdapters::PostgreSQL::Utils - def decode_string_array(value) - return value if value.is_a?(Array) - _arjdbc_array_parser.parse_pg_array(value) - end - - private - - def _arjdbc_array_parser - @_arjdbc_array_parser ||= begin - obj = Object.new - obj.extend(ActiveRecord::ConnectionAdapters::PostgreSQL::ArrayParser) - obj - end - end - - public - def foreign_keys(table_name) scope = quoted_scope(table_name) fk_info = internal_exec_query(<<~SQL, "SCHEMA", allow_retry: true, materialize_transactions: false) From 49a13bcff39def05c4d1f16ffa0cb1d26d37dfb9 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Mon, 4 May 2026 11:52:36 +1000 Subject: [PATCH 02/36] Pin rdoc to ~> 7 to fix JRuby native compilation issues Signed-off-by: Jesse Chavez --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 409024dd0..8f20f9d3f 100644 --- a/Gemfile +++ b/Gemfile @@ -105,5 +105,6 @@ group :rails do gem 'minitest-rg', require: nil gem 'minitest-rails', require: nil gem 'benchmark-ips', require: nil + gem 'rdoc', '~> 7', require: false end end From dab401bf97ca813443348e5b078c164d10f90e46 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 23 May 2026 08:33:38 +1000 Subject: [PATCH 03/36] Disable ReaperTest::test_reaper_works_after_pool_discard in JDBC due to deadlocks Signed-off-by: Jesse Chavez --- .../postgresql/ActiveRecord/ConnectionAdapters/ReaperTest.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/ReaperTest.rb b/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/ReaperTest.rb index 3f4f5c8e6..46049e4ad 100644 --- a/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/ReaperTest.rb +++ b/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/ReaperTest.rb @@ -1,2 +1,3 @@ exclude :test_some_time, 'intermittent failures, leaks thread, fires at high frequency' exclude :test_connection_pool_starts_reaper, 'intermittent failures, leaks thread, fires at high frequency' +exclude :test_reaper_works_after_pool_discard, 'deadlocks under JDBC due to high frequency discard' From b6a1bc35680fe915f63b8e9d53d10945dc81f171 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 17 Jun 2026 13:39:42 +1000 Subject: [PATCH 04/36] Disable broken :test_url_invalid_adapter due to added sqlserver adapter Signed-off-by: Jesse Chavez --- .../ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb | 1 + .../ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb | 1 + .../ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb | 1 + .../ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb | 1 + 4 files changed, 4 insertions(+) create mode 100644 test/rails/excludes/mysql2/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb create mode 100644 test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb create mode 100644 test/rails/excludes/sqlite3/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb create mode 100644 test/rails/excludes/sqlserver/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb diff --git a/test/rails/excludes/mysql2/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb b/test/rails/excludes/mysql2/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb new file mode 100644 index 000000000..ff8ea495d --- /dev/null +++ b/test/rails/excludes/mysql2/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb @@ -0,0 +1 @@ +exclude :test_url_invalid_adapter, "sqlserver is not a built-in adapter, so rails complains when validating the error" diff --git a/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb b/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb new file mode 100644 index 000000000..ff8ea495d --- /dev/null +++ b/test/rails/excludes/postgresql/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb @@ -0,0 +1 @@ +exclude :test_url_invalid_adapter, "sqlserver is not a built-in adapter, so rails complains when validating the error" diff --git a/test/rails/excludes/sqlite3/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb b/test/rails/excludes/sqlite3/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb new file mode 100644 index 000000000..ff8ea495d --- /dev/null +++ b/test/rails/excludes/sqlite3/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb @@ -0,0 +1 @@ +exclude :test_url_invalid_adapter, "sqlserver is not a built-in adapter, so rails complains when validating the error" diff --git a/test/rails/excludes/sqlserver/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb b/test/rails/excludes/sqlserver/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb new file mode 100644 index 000000000..ff8ea495d --- /dev/null +++ b/test/rails/excludes/sqlserver/ActiveRecord/ConnectionAdapters/PoolConfig/ResolverTest.rb @@ -0,0 +1 @@ +exclude :test_url_invalid_adapter, "sqlserver is not a built-in adapter, so rails complains when validating the error" From a3a866fd88d062c6a9060573d982ab6c3ccecfcc Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Tue, 5 May 2026 15:54:12 +1000 Subject: [PATCH 05/36] Force numeric/boolean to be encoded as varchar to mirror behaviour of PG gem Signed-off-by: Jesse Chavez --- .../arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index 51274ce95..353c64c0d 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -148,6 +148,15 @@ protected String buildURL(final ThreadContext context, final IRubyObject url) { return DriverWrapper.buildURL(url, Collections.EMPTY_MAP); } + @Override + protected Integer jdbcTypeForPrimitiveAttribute(final ThreadContext context, + final IRubyObject attribute) throws SQLException { + if (attribute instanceof RubyNumeric || attribute instanceof RubyBoolean) { + return Types.VARCHAR; + } + return super.jdbcTypeForPrimitiveAttribute(context, attribute); + } + @Override protected DriverWrapper newDriverWrapper(final ThreadContext context, final String driver) { DriverWrapper driverWrapper = super.newDriverWrapper(context, driver); From 0131de73651566ff9721ae7870c2d325d03b3e73 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 9 May 2026 04:22:08 +1000 Subject: [PATCH 06/36] Migrate client_min_messages in Postgres to use newer APIs Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index 88adbb878..589368205 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -596,8 +596,7 @@ def all_schemas # Returns the current client message level. def client_min_messages return nil if redshift? # not supported on Redshift - # Need to use #execute so we don't try to access the type map before it is initialized - execute('SHOW client_min_messages', 'SCHEMA').values.first.first + query_value("SHOW client_min_messages", "SCHEMA") end # Set the client message level. From c687b0c6258fe98cae33f84bb7cb5ec4c353d5b0 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 9 May 2026 05:24:52 +1000 Subject: [PATCH 07/36] Postgres now respects decode_dates setting Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 2 ++ .../postgresql/PostgreSQLRubyJdbcConnection.java | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index 589368205..9569572bf 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -104,6 +104,8 @@ def configure_connection end end + @raw_connection.decode_dates = decode_dates # Copy to java land for performance + reload_type_map end diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index 353c64c0d..d85bc123f 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -111,6 +111,7 @@ public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection private RubyClass resultClass; private RubyHash typeMap = null; + private boolean decodeDates = false; public PostgreSQLRubyJdbcConnection(Ruby runtime, RubyClass metaClass) { super(runtime, metaClass); @@ -813,6 +814,10 @@ protected IRubyObject dateToRuby(ThreadContext context, Ruby runtime, ResultSet final String value = resultSet.getString(index); if (value == null) return context.nil; + if (!decodeDates) { + return RubyString.newUnicodeString(runtime, value); + } + final int len = value.length(); if (len < 10 && value.charAt(len - 1) == 'y') { // infinity / -infinity IRubyObject infinity = parseInfinity(context.runtime, value); @@ -1093,4 +1098,10 @@ public IRubyObject typemap_set(ThreadContext context, IRubyObject mapArg) { this.typeMap = (RubyHash) mapArg; return mapArg; } + + @PG @JRubyMethod(name = "decode_dates=") + public IRubyObject setDecodeDates(ThreadContext context, IRubyObject value) { + this.decodeDates = value.isTrue(); + return value; + } } From 376e5625ec18bec0c823c874f57099cae2afabfa Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Mon, 11 May 2026 21:12:20 +1000 Subject: [PATCH 08/36] Fix Postgres disable_extension to work the same way enable_extenion works Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index 9569572bf..6c515f50e 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -327,6 +327,7 @@ def enable_extension(name, **) # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. def disable_extension(name, force: false) + _schema, name = name.to_s.split(".").values_at(-2, -1) internal_exec_query("DROP EXTENSION IF EXISTS \"#{name}\"#{' CASCADE' if force == :cascade}").tap { reload_type_map } From dfd7620eb983c8f6215f7e33c074b2a24f161d71 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Mon, 11 May 2026 21:26:47 +1000 Subject: [PATCH 09/36] Added fixednum lookup to Postgres columns for columnTypeMap to allow for index-based lookup for BSON access Mirrors upstream PG commit 100c86c00e Signed-off-by: Jesse Chavez --- src/java/arjdbc/postgresql/PostgreSQLResult.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/java/arjdbc/postgresql/PostgreSQLResult.java b/src/java/arjdbc/postgresql/PostgreSQLResult.java index 4643db42a..202f53da3 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLResult.java +++ b/src/java/arjdbc/postgresql/PostgreSQLResult.java @@ -107,7 +107,10 @@ protected IRubyObject columnTypeMap(final ThreadContext context) throws SQLExcep runtime.newFixnum(mod), name); - if (!type.isNil()) types.fastASet(name, type); + if (!type.isNil()) { + types.fastASet(name, type); + types.fastASet(runtime.newFixnum(i), type); + } } return types; From 246a6409bf5a354181abbe2f04d6319854383003 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Mon, 11 May 2026 21:57:41 +1000 Subject: [PATCH 10/36] Monkey patch to allow time value parsing to fall through fallback_string_to_time Signed-off-by: Jesse Chavez --- .../time_value_jruby_compat_monkey_patch.rb | 25 +++++++++++++++++++ lib/arjdbc/postgresql/adapter.rb | 1 + 2 files changed, 26 insertions(+) create mode 100644 lib/arjdbc/abstract/time_value_jruby_compat_monkey_patch.rb diff --git a/lib/arjdbc/abstract/time_value_jruby_compat_monkey_patch.rb b/lib/arjdbc/abstract/time_value_jruby_compat_monkey_patch.rb new file mode 100644 index 000000000..2d6e40424 --- /dev/null +++ b/lib/arjdbc/abstract/time_value_jruby_compat_monkey_patch.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "active_model/type/helpers/time_value" + +# The caller only handles ArgumentError as a failure, +# but JRuby raises TypeError for invalid formats in Time.new() (non-standard), +# which isn't handled by the caller. +# We just return nil here as it will result in fallback parsing, same as raising. +module ActiveModel + module Type + module Helpers + module TimeValue + module JRubyCompat + private + def fast_string_to_time(string) + super + rescue TypeError + nil + end + end + prepend JRubyCompat + end + end + end +end diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index 6c515f50e..d49182e23 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -29,6 +29,7 @@ require 'active_model' require "arjdbc/abstract/relation_query_attribute_monkey_patch" +require "arjdbc/abstract/time_value_jruby_compat_monkey_patch" module ArJdbc # Strives to provide Rails built-in PostgreSQL adapter (API) compatibility. From 84ca553ff1008bfac33eb00cab603ed4e49dec30 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Mon, 11 May 2026 23:44:16 +1000 Subject: [PATCH 11/36] Fix postgres not returning a result object for raw queries Signed-off-by: Jesse Chavez --- src/java/arjdbc/jdbc/JdbcResult.java | 10 +++++ src/java/arjdbc/jdbc/RubyJdbcConnection.java | 8 +++- .../arjdbc/postgresql/PostgreSQLResult.java | 41 +++++++++++++------ .../PostgreSQLRubyJdbcConnection.java | 16 ++++++++ 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/java/arjdbc/jdbc/JdbcResult.java b/src/java/arjdbc/jdbc/JdbcResult.java index 401f36dcc..03dde51b0 100644 --- a/src/java/arjdbc/jdbc/JdbcResult.java +++ b/src/java/arjdbc/jdbc/JdbcResult.java @@ -47,6 +47,16 @@ protected JdbcResult(ThreadContext context, RubyClass clazz, RubyJdbcConnection processResultSet(context, resultSet); } + // HACK: Needed for postgres to be able to return a sane result type instead of just a RubyFixnum + protected JdbcResult(ThreadContext context, RubyClass clazz, RubyJdbcConnection connection) { + super(context.runtime, clazz); + + values = newArray(context); + this.connection = connection; + columnNames = new RubyString[0]; + columnTypes = new int[0]; + } + /** * Builds a type map for creating the AR::Result, most adapters don't need it * @param context which thread this is running on. diff --git a/src/java/arjdbc/jdbc/RubyJdbcConnection.java b/src/java/arjdbc/jdbc/RubyJdbcConnection.java index d88d9a8d6..22b10ca7b 100644 --- a/src/java/arjdbc/jdbc/RubyJdbcConnection.java +++ b/src/java/arjdbc/jdbc/RubyJdbcConnection.java @@ -814,7 +814,7 @@ public IRubyObject execute(final ThreadContext context, final IRubyObject sql) { result = mapToRawResult(context, connection, resultSet, false); resultSet.close(); } else { - result = context.runtime.newFixnum(updateCount); + result = mapEmptyExecuteResult(context, updateCount); } // Check to see if there is another result set @@ -867,6 +867,10 @@ protected IRubyObject mapExecuteResult(final ThreadContext context, return mapQueryResult(context, connection, resultSet); } + protected IRubyObject mapEmptyExecuteResult(final ThreadContext context, final long updateCount) { + return newEmptyResult(context); + } + private static String[] createStatementPk(IRubyObject pk) { String[] statementPk; if (pk instanceof RubyArray) { @@ -3274,7 +3278,7 @@ protected boolean supportsGeneratedKeys(final Connection connection) throws SQLE * @param downCase should column names only be in lower case? */ @SuppressWarnings("unchecked") - private IRubyObject mapToRawResult(final ThreadContext context, + protected IRubyObject mapToRawResult(final ThreadContext context, final Connection connection, final ResultSet resultSet, final boolean downCase) throws SQLException { diff --git a/src/java/arjdbc/postgresql/PostgreSQLResult.java b/src/java/arjdbc/postgresql/PostgreSQLResult.java index 202f53da3..344c1fef9 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLResult.java +++ b/src/java/arjdbc/postgresql/PostgreSQLResult.java @@ -9,13 +9,7 @@ import java.sql.Types; import arjdbc.util.PG; -import org.jruby.Ruby; -import org.jruby.RubyArray; -import org.jruby.RubyClass; -import org.jruby.RubyHash; -import org.jruby.RubyModule; -import org.jruby.RubyNumeric; -import org.jruby.RubyString; +import org.jruby.*; import org.jruby.anno.JRubyMethod; import org.jruby.runtime.Block; import org.jruby.runtime.Helpers; @@ -39,6 +33,9 @@ public class PostgreSQLResult extends JdbcResult { // These are needed when generating an AR::Result private final ResultSetMetaData resultSetMetaData; + // An optional number of updated rows + private final long cmdTuples; + /********* JRuby compat methods ***********/ static RubyClass createPostgreSQLResultClass(ThreadContext context, RubyClass postgreSQLConnection) { @@ -64,6 +61,19 @@ static PostgreSQLResult newResult(ThreadContext context, RubyClass clazz, Postg return new PostgreSQLResult(context, clazz, connection, resultSet); } + /** + * Generates a new empty PostgreSQLResult object with a given number of updates + * @param context current thread context + * @param clazz metaclass for this result object + * @param updateCount the number of updated items + * @return an instantiated result object + * @throws SQLException throws! + */ + static PostgreSQLResult newEmptyResult(ThreadContext context, RubyClass clazz, PostgreSQLRubyJdbcConnection connection, + long updateCount) { + return new PostgreSQLResult(context, clazz, connection, updateCount); + } + /********* End JRuby compat methods ***********/ private PostgreSQLResult(ThreadContext context, RubyClass clazz, RubyJdbcConnection connection, @@ -71,6 +81,15 @@ private PostgreSQLResult(ThreadContext context, RubyClass clazz, RubyJdbcConnect super(context, clazz, connection, resultSet); resultSetMetaData = resultSet.getMetaData(); + cmdTuples = -1; + } + + private PostgreSQLResult(ThreadContext context, RubyClass clazz, RubyJdbcConnection connection, + long updateCount) { + super(context, clazz, connection); + + resultSetMetaData = null; + cmdTuples = updateCount; } /** @@ -261,12 +280,10 @@ public IRubyObject aref(ThreadContext context, IRubyObject rowArg) { return resultHash; } - // Note: this is # of commands (insert/update/selects performed) and not number of rows. In practice, - // so far users always just check this as to when it is 0 which ends up being the same as an update/insert - // where no rows were affected...so wrong value but the important value will be the same (I do not see - // how jdbc can do this). + // Note: This is probably not the best implementation, + // but it is better than always returning 0 on non-value-returning ops. @PG @JRubyMethod(name = {"cmdtuples", "cmd_tuples"}) public IRubyObject cmdtuples(ThreadContext context) { - return values.isEmpty() ? context.runtime.newFixnum(0) : aref(context, context.runtime.newFixnum(0)); + return cmdTuples != -1 ? context.runtime.newFixnum(cmdTuples) : values.length(context); } } diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index d85bc123f..341dac958 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -277,6 +277,22 @@ protected PostgreSQLResult mapExecuteResult(final ThreadContext context, final C return PostgreSQLResult.newResult(context, resultClass, this, resultSet); } + @Override + protected IRubyObject mapEmptyExecuteResult(final ThreadContext context, final long updateCount) { + return PostgreSQLResult.newEmptyResult(context, resultClass, this, updateCount); + } + + @Override + protected IRubyObject mapToRawResult(final ThreadContext context, + final Connection connection, final ResultSet resultSet, + final boolean downCase) throws SQLException { + if (downCase) { + return super.mapToRawResult(context, connection, resultSet, true); + } else { + return mapExecuteResult(context, connection, resultSet); + } + } + /** * Maps a query result set into a ActiveRecord result. * @param context From dc722ccd1e4181393e66d415896f490b3ebbdd9e Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 14 May 2026 09:52:08 +1000 Subject: [PATCH 12/36] Generalized timestamp infinity checking to support BigDecimal + other numeric types Signed-off-by: Jesse Chavez --- src/java/arjdbc/postgresql/PgDateTimeUtils.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/java/arjdbc/postgresql/PgDateTimeUtils.java b/src/java/arjdbc/postgresql/PgDateTimeUtils.java index 0890b0052..18ccee9a9 100644 --- a/src/java/arjdbc/postgresql/PgDateTimeUtils.java +++ b/src/java/arjdbc/postgresql/PgDateTimeUtils.java @@ -3,7 +3,8 @@ import arjdbc.util.DateTimeUtils; import org.joda.time.DateTimeZone; import org.jruby.RubyArray; -import org.jruby.RubyFloat; +import org.jruby.RubyNumeric; +import org.jruby.RubyFixnum; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; @@ -22,12 +23,14 @@ public abstract class PgDateTimeUtils extends DateTimeUtils { */ public static String timestampValueToString(final ThreadContext context, IRubyObject value, DateTimeZone zone, boolean withZone) { - if (value instanceof RubyFloat) { - final double dv = ((RubyFloat) value).getValue(); - if (dv == Double.POSITIVE_INFINITY) { - return "infinity"; - } else if (dv == Double.NEGATIVE_INFINITY) { - return "-infinity"; + if (value instanceof RubyNumeric valueNumeric) { + final IRubyObject infinite = valueNumeric.infinite_p(context); + if (infinite instanceof RubyFixnum infiniteFixnum) { + if (infiniteFixnum.getValue() > 0) { + return "infinity"; + } else { + return "-infinity"; + } } } return timestampTimeToString(context, value, zone, withZone); From 2b7118d723d60dabb97407821cf3696558dc3db0 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 14 May 2026 12:17:17 +1000 Subject: [PATCH 13/36] Update PostgreSQL::rename_enum to match rails implementation Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index d49182e23..dbb58c39e 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -409,10 +409,12 @@ def drop_enum(name, values = nil, **options) end # Rename an existing enum type to something else. - def rename_enum(name, options = {}) - to = options.fetch(:to) { raise ArgumentError, ":to is required" } + def rename_enum(name, new_name = nil, **options) + new_name ||= options.fetch(:to) do + raise ArgumentError, "rename_enum requires two from/to name positional arguments." + end - exec_query("ALTER TYPE #{quote_table_name(name)} RENAME TO #{to}").tap { reload_type_map } + exec_query("ALTER TYPE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}").tap { reload_type_map } end # Add enum value to an existing enum type. From 7c6208410cc59e72b2f6f07effc1686408ac0fc9 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 14 May 2026 12:24:43 +1000 Subject: [PATCH 14/36] Update PostgreSQL::add_enum_value to match rails implementation Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index dbb58c39e..1b4afb33e 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -420,14 +420,16 @@ def rename_enum(name, new_name = nil, **options) # Add enum value to an existing enum type. def add_enum_value(type_name, value, options = {}) before, after = options.values_at(:before, :after) - sql = +"ALTER TYPE #{quote_table_name(type_name)} ADD VALUE '#{value}'" + sql = +"ALTER TYPE #{quote_table_name(type_name)} ADD VALUE" + sql << " IF NOT EXISTS" if options[:if_not_exists] + sql << " #{quote(value)}" if before && after raise ArgumentError, "Cannot have both :before and :after at the same time" elsif before - sql << " BEFORE '#{before}'" + sql << " BEFORE #{quote(before)}" elsif after - sql << " AFTER '#{after}'" + sql << " AFTER #{quote(after)}" end execute(sql).tap { reload_type_map } From 6a4a88f7960b90beafbdc02ea43e6ce942af006c Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 14 May 2026 12:35:00 +1000 Subject: [PATCH 15/36] Use ruby-side type casting in array params for correct range literal handling Signed-off-by: Jesse Chavez --- .../PostgreSQLRubyJdbcConnection.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index 341dac958..fb716b611 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -52,6 +52,8 @@ import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.callsite.CachingCallSite; +import org.jruby.runtime.callsite.FunctionalCachingCallSite; import org.jruby.util.ByteList; import org.jruby.util.TypeConverter; @@ -324,13 +326,32 @@ protected void setArrayParameter(final ThreadContext context, break; } default: - values = valueForDB.toArray(); + final IRubyObject adapter = ActiveRecord(context).getClass(context, "Base").callMethod(context, "connection"); + values = typeCastArrayValues(context, adapter, valueForDB); break; } statement.setArray(index, connection.createArrayOf(typeName, values)); } + private final CachingCallSite type_cast_site = new FunctionalCachingCallSite("type_cast"); + + private Object[] typeCastArrayValues(final ThreadContext context, final IRubyObject adapter, final RubyArray values) { + final int size = values.size(); + final Object[] result = new Object[size]; + for (int i = 0; i < size; i++) { + final IRubyObject elem = values.eltInternal(i); + if (elem instanceof RubyArray arrayElem) { + result[i] = typeCastArrayValues(context, adapter, arrayElem); + } else { + // This could be a performance bottleneck, but it ensures that behaviour aligns with PG gem. Might want to revisit this later. + final IRubyObject cast = type_cast_site.call(context, adapter, adapter, elem); + result[i] = cast.isNil() ? null : cast.toJava(Object.class); + } + } + return result; + } + protected void setDecimalParameter(final ThreadContext context, final Connection connection, final PreparedStatement statement, final int index, final IRubyObject value, From 3a3e045657667dce7bb46d22c6e4b9f7db81abbf Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Fri, 15 May 2026 08:33:36 +1000 Subject: [PATCH 16/36] Fix dirty tracking when loading HStore from Postgres Signed-off-by: Jesse Chavez --- .../arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index fb716b611..bd4fb76f7 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -905,10 +905,9 @@ protected IRubyObject objectToRuby(ThreadContext context, Ruby runtime, ResultSe } if (object instanceof Map) { // hstore - // by default we avoid double parsing by driver and then column : - final RubyHash rubyObject = RubyHash.newHash(context.runtime); - rubyObject.putAll((Map) object); // converts keys/values to ruby - return rubyObject; + // This will be parsed by OID::HStore#deserialize + // Can't use hash as before due to hash breaking dirty tracking + return runtime.newString(resultSet.getString(index)); } return JavaUtil.convertJavaToRuby(runtime, object); From 02869571afe682e0ea848d5b61edbe606ccbef92 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Fri, 15 May 2026 08:58:01 +1000 Subject: [PATCH 17/36] Carry over set_constraints from rails Postgres driver Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/database_statements.rb | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/arjdbc/postgresql/database_statements.rb b/lib/arjdbc/postgresql/database_statements.rb index 2c1ddc85e..fbdaf139a 100644 --- a/lib/arjdbc/postgresql/database_statements.rb +++ b/lib/arjdbc/postgresql/database_statements.rb @@ -15,6 +15,27 @@ def build_explain_clause(options = []) "EXPLAIN (#{options.join(", ").upcase})" end + + # Set when constraints will be checked for the current transaction. + # + # Not passing any specific constraint names will set the value for all deferrable constraints. + # + # [deferred] + # Valid values are +:deferred+ or +:immediate+. + # + # See https://www.postgresql.org/docs/current/sql-set-constraints.html + def set_constraints(deferred, *constraints) + unless %i[deferred immediate].include?(deferred) + raise ArgumentError, "deferred must be :deferred or :immediate" + end + + constraints = if constraints.empty? + "ALL" + else + constraints.map { |c| quote_table_name(c) }.join(", ") + end + execute("SET CONSTRAINTS #{constraints} #{deferred.to_s.upcase}") + end end end end From 9929c0abccd4a411e09033e7c1d036df13e3d88c Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 20 May 2026 23:15:27 +1000 Subject: [PATCH 18/36] Match default PG gem stringtype connection param Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter_hash_config.rb | 3 +++ lib/arjdbc/postgresql/connection_methods.rb | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/arjdbc/postgresql/adapter_hash_config.rb b/lib/arjdbc/postgresql/adapter_hash_config.rb index bc042ac1f..413321831 100644 --- a/lib/arjdbc/postgresql/adapter_hash_config.rb +++ b/lib/arjdbc/postgresql/adapter_hash_config.rb @@ -92,6 +92,9 @@ def build_properties(config) properties["prepareThreshold"] = 0 end + # Match upstream default PG string type, otherwise incorrectly defaults to varchar + properties["stringtype"] ||= "unspecified" + properties end end diff --git a/lib/arjdbc/postgresql/connection_methods.rb b/lib/arjdbc/postgresql/connection_methods.rb index a1b5d7441..4a7190d9c 100644 --- a/lib/arjdbc/postgresql/connection_methods.rb +++ b/lib/arjdbc/postgresql/connection_methods.rb @@ -63,6 +63,9 @@ def postgresql_connection(config) properties['prepareThreshold'] = 0 end + # Match upstream default PG string type, otherwise incorrectly defaults to varchar + properties['stringtype'] ||= 'unspecified' + jdbc_connection(config) end alias_method :jdbcpostgresql_connection, :postgresql_connection From eb8975b9d20d3cfc83828904d17486a8b650459f Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 21 May 2026 00:02:42 +1000 Subject: [PATCH 19/36] Implement raw_exec_query to fix AsyncHasManyAssociationsTest#test_async_load_has_many Signed-off-by: Jesse Chavez --- lib/arjdbc/abstract/database_statements.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/arjdbc/abstract/database_statements.rb b/lib/arjdbc/abstract/database_statements.rb index 64a6a82f6..9fd2caf9a 100644 --- a/lib/arjdbc/abstract/database_statements.rb +++ b/lib/arjdbc/abstract/database_statements.rb @@ -38,16 +38,24 @@ def internal_exec_query(sql, name = nil, binds = NO_BINDS, prepare: false, async mark_transaction_written_if_write(sql) + raw_exec_query(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) + end + + def raw_exec_query(sql, name = nil, binds = NO_BINDS, prepare: false, async: false, allow_retry: false, materialize_transactions: true) binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array) - with_raw_connection do |conn| - if without_prepared_statement?(binds) - log(sql, name, async: async) { conn.execute_query(sql) } - else - log(sql, name, binds, async: async) do + # puts "[1]internal----->sql: #{sql}, binds: #{binds}" + type_casted_binds = type_casted_binds(binds) + # puts "[2]internal----->sql: #{type_casted_binds.size}, binds: #{type_casted_binds}" + + log(sql, name, binds, type_casted_binds, async: async) do + with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn| + if without_prepared_statement?(binds) + conn.execute_query(sql) + else # this is different from normal AR that always caches cached_statement = fetch_cached_statement(sql) if prepare && @jdbc_statement_cache_enabled - conn.execute_prepared_query(sql, binds, cached_statement) + conn.execute_prepared_query(sql, type_casted_binds, cached_statement) end end end From d4a6b20894ee54a9846bfab3083114d720dc1a75 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Fri, 22 May 2026 07:53:05 +1000 Subject: [PATCH 20/36] Migrate query logging to match upstream ruby call style and signatures Signed-off-by: Jesse Chavez --- lib/arjdbc/abstract/core.rb | 16 ---------------- lib/arjdbc/abstract/database_statements.rb | 8 +++++--- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/arjdbc/abstract/core.rb b/lib/arjdbc/abstract/core.rb index 2ed5d8c63..7806c5d6e 100644 --- a/lib/arjdbc/abstract/core.rb +++ b/lib/arjdbc/abstract/core.rb @@ -57,22 +57,6 @@ def translate_exception(exception, message:, sql:, binds:) end end - # this version of log() automatically fills type_casted_binds from binds if necessary - def log(sql, name = "SQL", binds = [], type_casted_binds = [], async: false, &block) - if binds.any? && (type_casted_binds.nil? || type_casted_binds.empty?) - type_casted_binds = lambda { - # extract_raw_bind_values - binds.map do |bind| - if bind.respond_to?(:value_for_database) - bind.value_for_database - else - bind - end - end - } - end - super - end end end diff --git a/lib/arjdbc/abstract/database_statements.rb b/lib/arjdbc/abstract/database_statements.rb index 9fd2caf9a..f80043ba1 100644 --- a/lib/arjdbc/abstract/database_statements.rb +++ b/lib/arjdbc/abstract/database_statements.rb @@ -22,8 +22,9 @@ def exec_insert(sql, name = nil, binds = NO_BINDS, pk = nil, sequence_name = nil if without_prepared_statement?(binds) log(sql, name) { conn.execute_insert_pk(sql, pk) } else - log(sql, name, binds) do - conn.execute_insert_pk(sql, binds, pk) + type_casted_binds = type_casted_binds(binds) + log(sql, name, binds, type_casted_binds) do + conn.execute_insert_pk(sql, type_casted_binds, pk) end end end @@ -74,7 +75,8 @@ def exec_update(sql, name = 'SQL', binds = NO_BINDS) if without_prepared_statement?(binds) log(sql, name) { conn.execute_update(sql) } else - log(sql, name, binds) { conn.execute_prepared_update(sql, binds) } + type_casted_binds = type_casted_binds(binds) + log(sql, name, binds, type_casted_binds) { conn.execute_prepared_update(sql, type_casted_binds) } end end end From 974f9d4c76b0442537fc5151a92f8a23c65c02af Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 23 May 2026 10:25:45 +1000 Subject: [PATCH 21/36] Switch to base rails preprocess_query call, remove now-redundant :check_if_write_query and :mark_transaction_written_if_write Signed-off-by: Jesse Chavez --- lib/arjdbc/abstract/database_statements.rb | 24 +++------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/lib/arjdbc/abstract/database_statements.rb b/lib/arjdbc/abstract/database_statements.rb index f80043ba1..0f88963f8 100644 --- a/lib/arjdbc/abstract/database_statements.rb +++ b/lib/arjdbc/abstract/database_statements.rb @@ -10,11 +10,7 @@ module DatabaseStatements NO_BINDS = [].freeze def exec_insert(sql, name = nil, binds = NO_BINDS, pk = nil, sequence_name = nil, returning: nil) - if preventing_writes? - raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" - end - - mark_transaction_written_if_write(sql) + sql = preprocess_query(sql) binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array) @@ -33,11 +29,7 @@ def exec_insert(sql, name = nil, binds = NO_BINDS, pk = nil, sequence_name = nil # It appears that at this point (AR 5.0) "prepare" should only ever be true # if prepared statements are enabled def internal_exec_query(sql, name = nil, binds = NO_BINDS, prepare: false, async: false, allow_retry: false, materialize_transactions: true) - if preventing_writes? && write_query?(sql) - raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" - end - - mark_transaction_written_if_write(sql) + sql = preprocess_query(sql) raw_exec_query(sql, name, binds, prepare: prepare, async: async, allow_retry: allow_retry, materialize_transactions: materialize_transactions) end @@ -63,11 +55,7 @@ def raw_exec_query(sql, name = nil, binds = NO_BINDS, prepare: false, async: fal end def exec_update(sql, name = 'SQL', binds = NO_BINDS) - if preventing_writes? - raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}" - end - - mark_transaction_written_if_write(sql) + sql = preprocess_query(sql) binds = convert_legacy_binds_to_attributes(binds) if binds.first.is_a?(Array) @@ -100,12 +88,6 @@ def convert_legacy_binds_to_attributes(binds) end end - def preprocess_query(sql) - check_if_write_query(sql) if respond_to?(:check_if_write_query, true) - mark_transaction_written_if_write(sql) if respond_to?(:mark_transaction_written_if_write, true) - sql - end - def raw_execute(sql, name, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false) log(sql, name, async: async) do with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn| From e9fe057012691ce90a30da87a63f78845cc3f67b Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 23 May 2026 11:45:29 +1000 Subject: [PATCH 22/36] Properly handle returning: kwarg in exec_insert Now mirrors built-in exec_insert, so it returns more than just the PK Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 16 ++++++++++------ lib/arjdbc/postgresql/database_statements.rb | 6 ++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index 1b4afb33e..c04cec96d 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -523,16 +523,20 @@ def check_version # :nodoc: end def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil, returning: nil) # :nodoc: - val = super - if !use_insert_returning? && pk + if use_insert_returning? || pk == false + sql, binds = sql_for_insert(sql, pk, binds, returning) + internal_exec_query(sql, name, binds) + else + result = internal_exec_query(sql, name, binds) unless sequence_name table_ref = extract_table_ref_from_insert_sql(sql) - sequence_name = default_sequence_name(table_ref, pk) - return val unless sequence_name + if table_ref + pk = primary_key(table_ref) if pk.nil? + sequence_name = default_sequence_name(table_ref, pk) + end + return result unless sequence_name end last_insert_id_result(sequence_name) - else - val end end diff --git a/lib/arjdbc/postgresql/database_statements.rb b/lib/arjdbc/postgresql/database_statements.rb index fbdaf139a..5d66db1b3 100644 --- a/lib/arjdbc/postgresql/database_statements.rb +++ b/lib/arjdbc/postgresql/database_statements.rb @@ -36,6 +36,12 @@ def set_constraints(deferred, *constraints) end execute("SET CONSTRAINTS #{constraints} #{deferred.to_s.upcase}") end + + private + + def returning_column_values(result) + result.rows.first + end end end end From 84b926edd2d4a30ae4e6280bcc05fe3f58b61d34 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 27 May 2026 00:06:25 +1000 Subject: [PATCH 23/36] Properly handle raw SQL for upserts Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index c04cec96d..f1bb3f3b6 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -508,8 +508,12 @@ def build_insert_sql(insert) # :nodoc: sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING" elsif insert.update_duplicates? sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET " - sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column} IS NOT DISTINCT FROM excluded.#{column}" } - sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",") + if insert.raw_update_sql? + sql << insert.raw_update_sql + else + sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column} IS NOT DISTINCT FROM excluded.#{column}" } + sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",") + end end sql << " RETURNING #{insert.returning}" if insert.returning From 58068df4eaf2be37da58086ee4c18e08c49dcc10 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 27 May 2026 21:07:30 +1000 Subject: [PATCH 24/36] Remove outdated and unneeded override of extract_table_ref_from_insert_sql from Postgres adapter Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index f1bb3f3b6..adb589beb 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -721,11 +721,6 @@ def column_definitions(table_name) SQL end - def extract_table_ref_from_insert_sql(sql) - sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] - $1.strip if $1 - end - def arel_visitor Arel::Visitors::PostgreSQL.new(self) end From 924b3462122e7985a302d1c0ac308b3ea776fbe0 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 28 May 2026 01:59:01 +1000 Subject: [PATCH 25/36] Remove outdated and unneeded override of default_sequence_name from Postgres adapter Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/adapter.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index adb589beb..fa56da676 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -591,12 +591,6 @@ def disconnect! end end - def default_sequence_name(table_name, pk = "id") #:nodoc: - serial_sequence(table_name, pk) - rescue ActiveRecord::StatementInvalid - %Q("#{table_name}_#{pk}_seq") - end - def last_insert_id_result(sequence_name) exec_query("SELECT currval('#{sequence_name}')", 'SQL') end From 95d6cd683767880ba9064f801a87676482d5d0f5 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 28 May 2026 03:27:23 +1000 Subject: [PATCH 26/36] Added default timezone to time/timestamp OID types Signed-off-by: Jesse Chavez --- lib/arjdbc/postgresql/oid_types.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/arjdbc/postgresql/oid_types.rb b/lib/arjdbc/postgresql/oid_types.rb index cd6e6cff9..8499d2203 100644 --- a/lib/arjdbc/postgresql/oid_types.rb +++ b/lib/arjdbc/postgresql/oid_types.rb @@ -147,8 +147,8 @@ def initialize_type_map_inner(m) m.register_type "regproc", OID::Enum.new # FIXME: adding this vector type leads to quoting not handlign Array data in quoting. #m.register_type "_int4", OID::Vector.new(",", m.lookup("int4")) - register_class_with_precision m, "time", Type::Time - register_class_with_precision m, "timestamp", OID::Timestamp + register_class_with_precision m, "time", Type::Time, timezone: @default_timezone + register_class_with_precision m, "timestamp", OID::Timestamp, timezone: @default_timezone register_class_with_precision m, "timestamptz", OID::TimestampWithTimeZone m.register_type "numeric" do |_, fmod, sql_type| From 0eda888a70e8e4162dd2b779cade71222fb14ef3 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Thu, 28 May 2026 03:35:50 +1000 Subject: [PATCH 27/36] Add JVM-compatible test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc Signed-off-by: Jesse Chavez --- test/rails/excludes/postgresql/BasicsTest.rb | 1 + test/simple.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/test/rails/excludes/postgresql/BasicsTest.rb b/test/rails/excludes/postgresql/BasicsTest.rb index ea57b1d0b..393cdaad3 100644 --- a/test/rails/excludes/postgresql/BasicsTest.rb +++ b/test/rails/excludes/postgresql/BasicsTest.rb @@ -1,4 +1,5 @@ [ # NOTE: these are copied to AR-JDBC's suite with proper (JVM) TZ adjustment + :test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc, :test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local, :test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local ].each do |name| diff --git a/test/simple.rb b/test/simple.rb index 2e6610d71..ddaa3b878 100644 --- a/test/simple.rb +++ b/test/simple.rb @@ -335,6 +335,22 @@ def test_time_with_default_timezone_local # + def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc + skip "with_system_tz not working in tomcat" if ActiveRecord::Base.connection.raw_connection.jndi? + + with_system_tz 'America/New_York' do # with_env_tz in Rails' tests + with_timezone_config default: :utc do + time = Time.local(2000) + record = DbType.create!('sample_datetime' => time) + saved_time = record.class.find(record.id).reload.sample_datetime + + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, 'EST'], time.to_a + assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, 'UTC'], saved_time.to_a + end + end + end + def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local skip "with_system_tz not working in tomcat" if ActiveRecord::Base.connection.raw_connection.jndi? From fda88d013ab8db92f18c0df3fa99b52686a217d1 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Tue, 2 Jun 2026 01:56:15 +1000 Subject: [PATCH 28/36] Exclude PessimisticLockingTest#test_with_lock_locks_with_no_args due to Arel bindings differing Signed-off-by: Jesse Chavez --- test/rails/excludes/postgresql/PessimisticLockingTest.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/rails/excludes/postgresql/PessimisticLockingTest.rb b/test/rails/excludes/postgresql/PessimisticLockingTest.rb index ad897f04d..3a55a8433 100644 --- a/test/rails/excludes/postgresql/PessimisticLockingTest.rb +++ b/test/rails/excludes/postgresql/PessimisticLockingTest.rb @@ -1 +1,2 @@ exclude :test_lock_sending_custom_lock_statement, 'AR looks for $1 when we use ?' if ActiveRecord::Base.lease_connection.prepared_statements +exclude :test_with_lock_locks_with_no_args, 'AR looks for $1 when we use ?' if ActiveRecord::Base.lease_connection.prepared_statements From ccde2ebd3860026d21a75d51d10efd8599ce1c03 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Tue, 2 Jun 2026 02:55:17 +1000 Subject: [PATCH 29/36] Handle Rails 8.0 removed Numeric#toTime Signed-off-by: Jesse Chavez --- src/java/arjdbc/postgresql/PgDateTimeUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/java/arjdbc/postgresql/PgDateTimeUtils.java b/src/java/arjdbc/postgresql/PgDateTimeUtils.java index 18ccee9a9..c07d46af1 100644 --- a/src/java/arjdbc/postgresql/PgDateTimeUtils.java +++ b/src/java/arjdbc/postgresql/PgDateTimeUtils.java @@ -4,6 +4,7 @@ import org.joda.time.DateTimeZone; import org.jruby.RubyArray; import org.jruby.RubyNumeric; +import org.jruby.RubyInteger; import org.jruby.RubyFixnum; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; @@ -32,6 +33,10 @@ public static String timestampValueToString(final ThreadContext context, IRubyOb return "-infinity"; } } + // Rails 8.0 removed Numeric#toTime, so we have to handle this here + if (valueNumeric instanceof RubyInteger) { + return valueNumeric.asString().toString(); + } } return timestampTimeToString(context, value, zone, withZone); } From 5bfebe0af5a84da4d006f3bc42cc10c0d0dd26c0 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Tue, 2 Jun 2026 03:05:09 +1000 Subject: [PATCH 30/36] Exclude broken msgpack-jruby tests Signed-off-by: Jesse Chavez --- test/rails/excludes/postgresql/ActiveRecordMessagePackTest.rb | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 test/rails/excludes/postgresql/ActiveRecordMessagePackTest.rb diff --git a/test/rails/excludes/postgresql/ActiveRecordMessagePackTest.rb b/test/rails/excludes/postgresql/ActiveRecordMessagePackTest.rb new file mode 100644 index 000000000..8b7515aad --- /dev/null +++ b/test/rails/excludes/postgresql/ActiveRecordMessagePackTest.rb @@ -0,0 +1,4 @@ +exclude :test_roundtrips_record_and_cached_associations, 'msgpack-jruby class resolution is busted, gets first valid instead of most applicable' +exclude :"test_roundtrips_new_record?_status", 'msgpack-jruby class resolution is busted, gets first valid instead of most applicable' +exclude :test_roundtrips_binary_attribute, 'msgpack-jruby class resolution is busted, gets first valid instead of most applicable' +exclude :"test_raises_ActiveSupport::MessagePack::MissingClassError_if_record_class_no_longer_exists", 'msgpack-jruby class resolution is busted, gets first valid instead of most applicable' From cb595b7e60029f5e97fb59f7878c0983ab7edf20 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 3 Jun 2026 07:16:01 +1000 Subject: [PATCH 31/36] Insert json as type OTHER to allow for coersion to column type Signed-off-by: Jesse Chavez --- src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index bd4fb76f7..f47d69919 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -659,10 +659,7 @@ private void setJsonParameter(final ThreadContext context, final PreparedStatement statement, final int index, final IRubyObject value, final String columnType) throws SQLException { - final PGobject pgJson = new PGobject(); - pgJson.setType(columnType); - pgJson.setValue(value.toString()); - statement.setObject(index, pgJson); + statement.setObject(index, value.toString(), Types.OTHER); } private void setPGobjectParameter(final PreparedStatement statement, final int index, From 1831186ca0ce3794b527f1596671b47ad00aac2d Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 3 Jun 2026 07:41:32 +1000 Subject: [PATCH 32/36] Properly set timezone when setting timestamp params Signed-off-by: Jesse Chavez --- src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index f47d69919..bc63a101f 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -394,7 +394,7 @@ protected void setTimestampParameter(final ThreadContext context, final int index, IRubyObject value, final IRubyObject attribute, final int type) throws SQLException { // PGJDBC uses strings internally anyway, so using Timestamp doesn't do any good - String tsString = PgDateTimeUtils.timestampValueToString(context, value, null, true); + String tsString = PgDateTimeUtils.timestampValueToString(context, value, getDefaultTimeZone(context), true); statement.setObject(index, tsString, Types.OTHER); } From c672e9ffd9d45608ddfdec19fd484a47ae351ff4 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 3 Jun 2026 08:23:50 +1000 Subject: [PATCH 33/36] Disabled marshalling tests that are broken with AR format 6.1 + JRuby 10.0.5.0 Signed-off-by: Jesse Chavez --- .../excludes/postgresql/AssociationsExtensionsTest.rb | 6 ++++++ test/rails/excludes/postgresql/BasicsTest.rb | 7 +++++++ .../postgresql/HasAndBelongsToManyAssociationsTest.rb | 1 + .../excludes/postgresql/HasManyThroughAssociationsTest.rb | 1 + test/rails/excludes/postgresql/MarshalSerializationTest.rb | 1 + test/rails/excludes/postgresql/RelationMergingTest.rb | 1 + test/rails/excludes/postgresql/SanitizeTest.rb | 1 + 7 files changed, 18 insertions(+) create mode 100644 test/rails/excludes/postgresql/AssociationsExtensionsTest.rb create mode 100644 test/rails/excludes/postgresql/HasAndBelongsToManyAssociationsTest.rb create mode 100644 test/rails/excludes/postgresql/HasManyThroughAssociationsTest.rb create mode 100644 test/rails/excludes/postgresql/MarshalSerializationTest.rb create mode 100644 test/rails/excludes/postgresql/RelationMergingTest.rb create mode 100644 test/rails/excludes/postgresql/SanitizeTest.rb diff --git a/test/rails/excludes/postgresql/AssociationsExtensionsTest.rb b/test/rails/excludes/postgresql/AssociationsExtensionsTest.rb new file mode 100644 index 000000000..66987968f --- /dev/null +++ b/test/rails/excludes/postgresql/AssociationsExtensionsTest.rb @@ -0,0 +1,6 @@ +[ + :test_marshalling_extensions, + :test_marshalling_named_extensions +].each do |name| + exclude name, 'Activerecord 6.1 marshalling format is broken with JRuby 10.0.5.0 (cyclic refs with custom marshal) - AR 7.1 marshal works fine' +end diff --git a/test/rails/excludes/postgresql/BasicsTest.rb b/test/rails/excludes/postgresql/BasicsTest.rb index 393cdaad3..044a91cc5 100644 --- a/test/rails/excludes/postgresql/BasicsTest.rb +++ b/test/rails/excludes/postgresql/BasicsTest.rb @@ -5,3 +5,10 @@ ].each do |name| exclude name, 'assuming ENV[TZ] change reflects system (JVM) TimeZone default change' end + +[ + :test_marshalling_with_associations_6_1, + :test_marshalling_new_record_round_trip_with_associations +].each do |name| + exclude name, 'Activerecord 6.1 marshalling format is broken with JRuby 10.0.5.0 (cyclic refs with custom marshal) - AR 7.1 marshal works fine' +end diff --git a/test/rails/excludes/postgresql/HasAndBelongsToManyAssociationsTest.rb b/test/rails/excludes/postgresql/HasAndBelongsToManyAssociationsTest.rb new file mode 100644 index 000000000..1bc1dda33 --- /dev/null +++ b/test/rails/excludes/postgresql/HasAndBelongsToManyAssociationsTest.rb @@ -0,0 +1 @@ +exclude :test_marshal_dump, 'Activerecord 6.1 marshalling format is broken with JRuby 10.0.5.0 (cyclic refs with custom marshal) - AR 7.1 marshal works fine' diff --git a/test/rails/excludes/postgresql/HasManyThroughAssociationsTest.rb b/test/rails/excludes/postgresql/HasManyThroughAssociationsTest.rb new file mode 100644 index 000000000..1bc1dda33 --- /dev/null +++ b/test/rails/excludes/postgresql/HasManyThroughAssociationsTest.rb @@ -0,0 +1 @@ +exclude :test_marshal_dump, 'Activerecord 6.1 marshalling format is broken with JRuby 10.0.5.0 (cyclic refs with custom marshal) - AR 7.1 marshal works fine' diff --git a/test/rails/excludes/postgresql/MarshalSerializationTest.rb b/test/rails/excludes/postgresql/MarshalSerializationTest.rb new file mode 100644 index 000000000..bd9c58230 --- /dev/null +++ b/test/rails/excludes/postgresql/MarshalSerializationTest.rb @@ -0,0 +1 @@ +exclude :test_rails_6_1_rountrip, 'Activerecord 6.1 marshalling format is broken with JRuby 10.0.5.0 (cyclic refs with custom marshal) - AR 7.1 marshal works fine' diff --git a/test/rails/excludes/postgresql/RelationMergingTest.rb b/test/rails/excludes/postgresql/RelationMergingTest.rb new file mode 100644 index 000000000..b4f3eb5ac --- /dev/null +++ b/test/rails/excludes/postgresql/RelationMergingTest.rb @@ -0,0 +1 @@ +exclude :test_merge_doesnt_duplicate_same_clauses, 'AR looks for $1 when we use ?' if ActiveRecord::Base.lease_connection.prepared_statements diff --git a/test/rails/excludes/postgresql/SanitizeTest.rb b/test/rails/excludes/postgresql/SanitizeTest.rb new file mode 100644 index 000000000..71fa2521b --- /dev/null +++ b/test/rails/excludes/postgresql/SanitizeTest.rb @@ -0,0 +1 @@ +exclude :test_sanitize_sql_like_example_use_case, 'AR looks for $1 when we use ?' if ActiveRecord::Base.lease_connection.prepared_statements From 9accad2c67bbcdcdc654aeff9e59b979e0b30f93 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 6 Jun 2026 08:22:20 +1000 Subject: [PATCH 34/36] Monkeypatch to work around bug in prism logger polyfill breaking tests Signed-off-by: Jesse Chavez --- .../mock_logger_jruby_compat_monkey_patch.rb | 25 +++++++++++++++++++ lib/arjdbc/mysql/adapter.rb | 1 + lib/arjdbc/postgresql/adapter.rb | 1 + lib/arjdbc/sqlite3/adapter.rb | 1 + 4 files changed, 28 insertions(+) create mode 100644 lib/arjdbc/abstract/mock_logger_jruby_compat_monkey_patch.rb diff --git a/lib/arjdbc/abstract/mock_logger_jruby_compat_monkey_patch.rb b/lib/arjdbc/abstract/mock_logger_jruby_compat_monkey_patch.rb new file mode 100644 index 000000000..3af511f2b --- /dev/null +++ b/lib/arjdbc/abstract/mock_logger_jruby_compat_monkey_patch.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require "active_support/log_subscriber/test_helper" + +# As of prism 1.9.0 (possibly earlier), a polyfill exists to handle logging warnings for ruby impls that lack `category:`. +# As a result of this injection being a public method (as opposed to CRuby's private version), +# on at least JRuby 10.0.5.0, MockLogger#method_missing never gets hit, which breaks a number of tests. +# This monkeypatch adds support to the MockLogger to properly capture these log events in spite of this difference. +module ActiveSupport + class LogSubscriber + module TestHelper + class MockLogger + module JRubyCompat + ActiveSupport::Logger::Severity.constants.each do |severity| + level = severity.downcase + define_method(level) do |message = nil, &block| + @logged[level] << (block ? block.call : message) + end + end + end + prepend JRubyCompat + end + end + end +end diff --git a/lib/arjdbc/mysql/adapter.rb b/lib/arjdbc/mysql/adapter.rb index 276205b00..06eab0d14 100644 --- a/lib/arjdbc/mysql/adapter.rb +++ b/lib/arjdbc/mysql/adapter.rb @@ -14,6 +14,7 @@ require "arjdbc/mysql/adapter_hash_config" require "arjdbc/abstract/relation_query_attribute_monkey_patch" +require "arjdbc/abstract/mock_logger_jruby_compat_monkey_patch" module ActiveRecord module ConnectionAdapters diff --git a/lib/arjdbc/postgresql/adapter.rb b/lib/arjdbc/postgresql/adapter.rb index fa56da676..9a3400732 100644 --- a/lib/arjdbc/postgresql/adapter.rb +++ b/lib/arjdbc/postgresql/adapter.rb @@ -30,6 +30,7 @@ require "arjdbc/abstract/relation_query_attribute_monkey_patch" require "arjdbc/abstract/time_value_jruby_compat_monkey_patch" +require "arjdbc/abstract/mock_logger_jruby_compat_monkey_patch" module ArJdbc # Strives to provide Rails built-in PostgreSQL adapter (API) compatibility. diff --git a/lib/arjdbc/sqlite3/adapter.rb b/lib/arjdbc/sqlite3/adapter.rb index b97f73f73..438be21df 100644 --- a/lib/arjdbc/sqlite3/adapter.rb +++ b/lib/arjdbc/sqlite3/adapter.rb @@ -21,6 +21,7 @@ require "arjdbc/sqlite3/pragmas" require "arjdbc/abstract/relation_query_attribute_monkey_patch" +require "arjdbc/abstract/mock_logger_jruby_compat_monkey_patch" module SQLite3 module Constants From 7e6d672fe300b804aa96cad437cb43d5fb0eb6dc Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Sat, 6 Jun 2026 09:48:13 +1000 Subject: [PATCH 35/36] Synchronize ActiveSupport callback registration in tests to compensate for a lack of a GIL Signed-off-by: Jesse Chavez --- test/rails/active_support/callbacks.rb | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/rails/active_support/callbacks.rb diff --git a/test/rails/active_support/callbacks.rb b/test/rails/active_support/callbacks.rb new file mode 100644 index 000000000..82e86f9ef --- /dev/null +++ b/test/rails/active_support/callbacks.rb @@ -0,0 +1,52 @@ +# Partial workaround for JRuby having no GIL - tests like the reaper etc spuriously fail due to multithreaded test execution +# assuming a GIL that does not actually exist for JRuby. We really do love non-deterministic multithreaded execution, +# it never, ever causes random bugs that spuriously fail without any consistent way to reproduce them! +# Multithreading. Not even once. +# Do we want to make this happen both in and out of tests? Might be worth considering. + +# We want to inject *after* this is loaded, so gonna load it first. There's probably a better way to do this. +real = $LOAD_PATH + .map { |dir| File.expand_path(File.join(dir, "active_support", "callbacks.rb")) } + .find { |path| path != File.expand_path(__FILE__) && File.exist?(path) } +require real + +require "monitor" + +module ActiveSupport + module Callbacks + module ClassMethods + REGISTRATION_MONITOR = Monitor.new + + module ThreadSafeRegistration + def set_callback(*, &block) + REGISTRATION_MONITOR.synchronize { super } + end + + def skip_callback(*, &block) + REGISTRATION_MONITOR.synchronize { super } + end + + def reset_callbacks(*) + REGISTRATION_MONITOR.synchronize { super } + end + + def define_callbacks(*) + REGISTRATION_MONITOR.synchronize { super } + end + + protected + # Publish by atomic reference swap rather than the in-place mutation the + # original performs, so unlocked readers never see a torn hash. + def set_callbacks(name, callbacks) + REGISTRATION_MONITOR.synchronize do + new_callbacks = __callbacks.dup + new_callbacks[name.to_sym] = callbacks + self.__callbacks = new_callbacks + end + end + end + + prepend ThreadSafeRegistration + end + end +end From bbeafdc3c9979002d03c03dae36a5297b6f457d8 Mon Sep 17 00:00:00 2001 From: Andrew Silver Date: Wed, 17 Jun 2026 11:20:01 +1000 Subject: [PATCH 36/36] Add support for PG bind-param hash in blob params Signed-off-by: Jesse Chavez --- .../arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java index bc63a101f..3dabcc036 100644 --- a/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +++ b/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java @@ -156,6 +156,8 @@ protected Integer jdbcTypeForPrimitiveAttribute(final ThreadContext context, final IRubyObject attribute) throws SQLException { if (attribute instanceof RubyNumeric || attribute instanceof RubyBoolean) { return Types.VARCHAR; + } else if (attribute instanceof RubyHash) { // Should be a pg-style bind-param hash + return Types.BINARY; } return super.jdbcTypeForPrimitiveAttribute(context, attribute); } @@ -379,8 +381,11 @@ protected void setBlobParameter(final ThreadContext context, if ( value instanceof RubyIO ) { // IO/File statement.setBinaryStream(index, ((RubyIO) value).getInStream()); } - else { // should be a RubyString - final ByteList bytes = value.asString().getByteList(); + else { // should be a RubyString, or pg-style bind-param hash + final IRubyObject binary = value instanceof RubyHash hashValue + ? hashValue.op_aref(context, context.runtime.newSymbol("value")) + : value; + final ByteList bytes = binary.asString().getByteList(); statement.setBinaryStream(index, new ByteArrayInputStream(bytes.unsafeBytes(), bytes.getBegin(), bytes.getRealSize()), bytes.getRealSize() // length