Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ActiveRecord::Store with JSON column uses nil as default value instead of {} #50960

Open
sambostock opened this issue Feb 4, 2024 · 1 comment · May be fixed by #51864
Open

ActiveRecord::Store with JSON column uses nil as default value instead of {} #50960

sambostock opened this issue Feb 4, 2024 · 1 comment · May be fixed by #51864

Comments

@sambostock
Copy link
Contributor

Steps to reproduce

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails" # , github: "rails/rails", branch: "main"

  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :nullable_settings, force: true do |t|
    t.json :settings
  end

  create_table :non_nullable_settings, force: true do |t|
    t.json :settings, null: false
  end
end

class BugTest < ActiveSupport::TestCase
  self.test_order = :sorted # Not required, but makes it easier to look at the debug output.

  setup { ActiveRecord::Base.logger.debug(name) }

  test "1_creating nullable settings without settings saves nil" do
    assert_stores_nil(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "nullable_settings"
    end)
  end

  test "2_creating non-nullable settings without settings raises error" do
    assert_raises_not_null_violation(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "non_nullable_settings"
    end)
  end

  test "3_creating nullable settings with attribute default saves empty hash" do
    assert_stores_empty_hash(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "nullable_settings"
      model.attribute :settings, :json, default: {}
    end)
  end

  test "4_creating non-nullable settings with attribute default saves empty hash" do
    assert_stores_empty_hash(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "non_nullable_settings"
      model.attribute :settings, :json, default: {}
    end)
  end

  test "5_creating nullable settings with store saves nil" do
    assert_stores_nil(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "nullable_settings"
      model.store :settings
    end)
  end

  test "6_creating non-nullable settings with store raises error" do
    assert_stores_empty_hash(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "non_nullable_settings"
      model.store :settings
    end) # fails (NotNullViolation)
  end

  test "7_creating nullable settings with store and attribute default saves nil" do
    assert_stores_empty_hash(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "nullable_settings"
      model.attribute :settings, :json, default: {}
      model.store :settings
    end) # fails (nil, not {})
  end

  test "8_creating non-nullable settings with store and attribute default raises error" do
    assert_stores_empty_hash(Class.new(ActiveRecord::Base) do |model|
      model.table_name = "non_nullable_settings"
      model.attribute :settings, :json, default: {}
      model.store :settings
    end) # fails (NotNullViolation)
  end

  private

  def assert_stores_nil(model)                = assert_nil(model.create!.settings_before_type_cast)
  def assert_raises_not_null_violation(model) = assert_raises(ActiveRecord::NotNullViolation) { model.create! }

  def assert_stores_empty_hash(model)
    assert_equal("{}", model.create!.settings_before_type_cast)
  rescue ActiveRecord::NotNullViolation => error
    flunk error.message
  end
end

Expected behavior

When using ActiveRecord::Store with a column of type json, the default value written to the database should be an empty Hash (serialized as an empty JSON object {}), especially if the attribute is marked as having a default value of {}.

Actual behavior

When using ActiveRecord::Store with a column of type json, the default value written to the database is nil.

This is problematic when using non-nullable JSON columns, which cannot accept defaults at the database schema level, as there is no way to avoid violating the NULL constraint.

System configuration

Rails version: rails 7.1.3

Ruby version: ruby 3.3.0

@rails-bot
Copy link

rails-bot bot commented May 8, 2024

This issue has been automatically marked as stale because it has not been commented on for at least three months.
The resources of the Rails team are limited, and so we are asking for your help.
If you can still reproduce this error on the 7-1-stable branch or on main, please reply with all of the information you have about it in order to keep the issue open.
Thank you for all your contributions.

@rails-bot rails-bot bot added the stale label May 8, 2024
@rails-bot rails-bot bot closed this as completed May 15, 2024
@zzak zzak reopened this May 18, 2024
@rails-bot rails-bot bot removed the stale label May 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants