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 · 0 comments

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants