diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index cd01fe1e9c43f..60166100da5be 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add `Rails.application.config.active_record.postgresql_adapter_decode_dates` to opt out of decoding dates automatically with the postgresql adapter. Defaults to true. + + *JoƩ Dupuis* + * Add ENV["SKIP_TEST_DATABASE_TRUNCATE"] flag to speed up multi-process test runs on large DBs when all tests run within default txn. (This cuts ~10s from the test run of HEY when run by 24 processes against the 178 tables, since ~4,000 table truncates can then be skipped.) *DHH* diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 114324d38da5f..197cd416014d6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -122,6 +122,15 @@ def dbconsole(config, options = {}) # setting, you should immediately run bin/rails db:migrate to update the types in your schema.rb. class_attribute :datetime_type, default: :timestamp + ## + # :singleton-method: + # Toggles automatic decoding of date columns. + # + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date").class #=> String + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.decode_dates = true + # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date").class #=> Date + class_attribute :decode_dates, default: false + NATIVE_DATABASE_TYPES = { primary_key: "bigserial primary key", string: { name: "character varying" }, @@ -1159,8 +1168,8 @@ def add_pg_decoders "bool" => PG::TextDecoder::Boolean, "timestamp" => PG::TextDecoder::TimestampUtc, "timestamptz" => PG::TextDecoder::TimestampWithTimeZone, - "date" => PG::TextDecoder::Date, } + coders_by_name["date"] = PG::TextDecoder::Date if decode_dates known_coder_types = coders_by_name.keys.map { |n| quote(n) } query = <<~SQL % known_coder_types.join(", ") diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 21170967edfdf..ee7477dbdc801 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -217,6 +217,16 @@ class Railtie < Rails::Railtie # :nodoc: end end + initializer "active_record.postgresql_adapter_decode_dates" do + config.after_initialize do + if config.active_record.postgresql_adapter_decode_dates + ActiveSupport.on_load(:active_record_postgresqladapter) do + self.decode_dates = true + end + end + end + end + initializer "active_record.set_configs" do |app| configs = app.config.active_record @@ -245,7 +255,8 @@ class Railtie < Rails::Railtie # :nodoc: :cache_query_log_tags, :sqlite3_adapter_strict_strings_by_default, :check_schema_cache_dump_version, - :use_schema_cache_dump + :use_schema_cache_dump, + :postgresql_adapter_decode_dates, ) configs.each do |k, v| diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb index 3afbf78f566a8..cb9f3be5187a1 100644 --- a/activerecord/test/cases/adapters/postgresql/date_test.rb +++ b/activerecord/test/cases/adapters/postgresql/date_test.rb @@ -39,10 +39,4 @@ def test_bc_date_year_zero topic = Topic.create!(last_read: date) assert_equal date, Topic.find(topic.id).last_read end - - def test_date_decoder - date = ActiveRecord::Base.connection.select_value("select '2024-01-01'::date") - assert_equal Date.new(2024, 01, 01), date - assert_equal Date, date.class - end end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 879d3996f7a33..f5f1a5d937a7b 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -649,7 +649,25 @@ def test_does_not_raise_notice_level_warnings end end + def test_date_decoder + db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary") + connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(db_config.configuration_hash) + + with_postgresql_apdater_decode_dates do + date = connection.select_value("select '2024-01-01'::date") + assert_equal Date.new(2024, 01, 01), date + assert_equal Date, date.class + end + end + private + def with_postgresql_apdater_decode_dates + PostgreSQLAdapter.decode_dates = true + yield + ensure + PostgreSQLAdapter.decode_dates = false + end + def with_example_table(definition = "id serial primary key, number integer, data character varying(255)", &block) super(@connection, "ex", definition, &block) end diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 547bf01b1d9c1..e6fb73113455b 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -63,6 +63,7 @@ Below are the default values associated with each target version. In cases of co - [`config.active_job.enqueue_after_transaction_commit`](#config-active-job-enqueue-after-transaction-commit): `:default` - [`config.active_record.automatically_invert_plural_associations`](#config-active-record-automatically-invert-plural-associations): `true` - [`config.active_record.validate_migration_timestamps`](#config-active-record-validate-migration-timestamps): `true` +- [`config.active_record.postgresql_adapter_decode_dates`](#config-active-record-postgresql-adapter-decode-dates): `true` - [`config.active_storage.web_image_content_types`](#config-active-storage-web-image-content-types): `%w[image/png image/jpeg image/gif image/webp]` #### Default Values for Target Version 7.1 @@ -1490,6 +1491,25 @@ The default value depends on the `config.load_defaults` target version: | (original) | `false` | | 7.1 | `true` | +#### `config.active_record.postgresql_adapter_decode_dates` + +Specifies whether the PostgresqlAdapter should decode date columns. + +Ex: +```ruby +ActiveRecord::Base.connection + .select_value("select '2024-01-01'::date").class #=> Date +``` + + +The default value depends on the `config.load_defaults` target version: + +| Starting with version | The default value is | +| --------------------- | -------------------- | +| (original) | `false` | +| 7.2 | `true` | + + #### `config.active_record.async_query_executor` Specifies how asynchronous queries are pooled. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 665c7e689d900..fdcfdc0144a74 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -81,6 +81,17 @@ Upgrading from Rails 7.1 to Rails 7.2 For more information on changes made to Rails 7.2 please see the [release notes](7_2_release_notes.html). +### `PostgresqlAdapter` now decodes dates automatically + +Manual queries with columns of type date will automatically be decoded to `Date` objects. +To disable this new behavior and return the dates as `String`: + +```ruby +# config/application.rb +config.active_record.postgresql_adapter_decode_dates = false +``` + + Upgrading from Rails 7.0 to Rails 7.1 ------------------------------------- diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 8d87931b00a50..8cfb477a233f7 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -327,6 +327,7 @@ def load_defaults(target_version) end if respond_to?(:active_record) + active_record.postgresql_adapter_decode_dates = true active_record.validate_migration_timestamps = true active_record.automatically_invert_plural_associations = true end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_2.rb.tt index 48552491e9634..fb3cd5770dfff 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_2.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_2.rb.tt @@ -66,3 +66,13 @@ # on `Post`. With this option enabled, it will also look for a `:comments` association. #++ # Rails.application.config.active_record.automatically_invert_plural_associations = true + +### +# Controls whether the PostgresqlAdapter should decode dates automatically with manual queries. +# +# Example: +# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date") #=> Date +# +# This query used to return a `String`. +#++ +# Rails.application.config.active_record.postgresql_adapter_decode_dates = true diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 5d531cedf85a5..4f990674d227d 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -2793,6 +2793,40 @@ def index assert_equal false, ActiveRecord::Base.run_commit_callbacks_on_first_saved_instances_in_transaction end + test "PostgresqlAdapter.decode_dates is true by default for new apps" do + app_file "config/initializers/active_record.rb", <<~RUBY + ActiveRecord::Base.establish_connection(adapter: "postgresql", database: ":memory:") + RUBY + + app "development" + + assert_equal true, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.decode_dates + end + + test "PostgresqlAdapter.decode_dates is false by default for upgraded apps" do + remove_from_config '.*config\.load_defaults.*\n' + app_file "config/initializers/active_record.rb", <<~RUBY + ActiveRecord::Base.establish_connection(adapter: "postgresql", database: ":memory:") + RUBY + + app "development" + + assert_equal false, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.decode_dates + end + + test "PostgresqlAdapter.decode_dates can be configured via config.active_record.postgresql_adapter_decode_dates" do + remove_from_config '.*config\.load_defaults.*\n' + add_to_config "config.active_record.postgresql_adapter_decode_dates = true" + + app_file "config/initializers/active_record.rb", <<~RUBY + ActiveRecord::Base.establish_connection(adapter: "postgresql", database: ":memory:") + RUBY + + app "development" + + assert_equal true, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.decode_dates + end + test "SQLite3Adapter.strict_strings_by_default is true by default for new apps" do app_file "config/initializers/active_record.rb", <<~RUBY ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")