Active Record fetching default column value is too magical

I wanted to add a new flag with a static default value to my DB table, but change the default value in Ruby based on certain conditions.

add_column :records, :new_flag, :boolean, null: false, default: true
class Record < ActiveRecord::Base
  after_initialize do
    self.new_flag = some_expression if new_flag.nil?
  end
end

However, I was surprised to find out that new_flag will never be nil in this case, because for new model instances Active Record will populate the column attribute with the default DB value, which in this case is true.

The problem with this is that I don’t know how to distinguish this default DB value from user input in a general case. I don’t know if the user has set new_flag to true in a form, or they haven’t set anything and true comes from the DB default.

People sometimes complain Rails is too magical, and I remember some Rails core team members saying they cannot really do anything with this information. So, I wanted to point out a specific example of what I find too magical. For comparison, Sequel doesn’t do this by default, but it has a plugin to get this behavior.

#{attribute_name}_came_from_user? method should help with that.

Reproduction script:
# 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 :records, force: true do |t|
    t.boolean :new_flag, null: false, default: true
  end
end

class Record < ActiveRecord::Base
end


class BugTest < Minitest::Test
  def test_value_came_from_user
    user_passed_value = Record.new(new_flag: true)
    assert_predicate user_passed_value, :new_flag
    assert_predicate user_passed_value, :new_flag_came_from_user?

    default_value = Record.new
    assert_predicate default_value, :new_flag
    refute_predicate default_value, :new_flag_came_from_user?
  end
end

It’s not publicly documented and unfortunately it seems like it was intentional but I wonder if it could be reconsidered as your use-case seems to be valid Only use the `_before_type_cast` in the form when from user input · rails/rails@d8e7104 · GitHub https://github.com/rails/rails/blob/main/activerecord/lib/active_record/attribute_methods/before_type_cast.rb#L33

Personally I would argue for having no default in the database and entirely determine the default within Ruby. You are wanting the default to be partially dynamic and partially hard-coded. Seems simpler IMHO to just make it entirely dynamic.

Curious, what behavior were you expecting? Was it that Record.new.new_flag is always nil, but Record.new.tap(&:save).new_flag is the default from db?

Would it work then if in your after_initialize you check new_record? instead of new_flag.nil??

checking .new_record? is definitely the right thing to do here.

You can also look at .changed?

In the case of a new record created with rec = Record.new(new_flag: "x"), .changed? will be true. When you create a record without overriding the default value for new_flag, .changed? will be false

The after_initialize will leave .changed? as false.