Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion lib/active_record/virtual_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def virtual_column(name, type:, **options)
virtual_attribute(name, type, **options)
end

def virtual_attribute(name, type, through: nil, uses: nil, arel: nil, source: name, default: nil, **options)
def virtual_attribute(name, type, through: nil, uses: nil, arel: nil, source: name, default: nil, **options, &block)
name = name.to_s
reload_schema_from_cache

Expand All @@ -77,6 +77,10 @@ def virtual_attribute(name, type, through: nil, uses: nil, arel: nil, source: na
# Because we may not have loaded the class yet
# And we definitely have not loaded the database yet
arel ||= virtual_delegate_arel(source, to_ref)
elsif block_given?
define_method(name) do
has_attribute?(name) ? self[name] : instance_eval(&block)
Copy link
Member Author

@kbrock kbrock Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so nice.

Before

We can do minimal common manipulations for sql in the TableProxy, but can't do any common manipulation for the ruby code.

After

Adding stuff to ruby like the has_attribute? pattern is a big win.
Besides cutting down on the noise, we often forget to add this block and forgetting it can cause problems at runtime.

end
end

define_virtual_include(name, uses)
Expand Down
36 changes: 14 additions & 22 deletions spec/db/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,32 +61,19 @@ def books_with_authors
virtual_total :total_named_books, :named_books
alias v_total_named_books total_named_books

def nick_or_name
has_attribute?("nick_or_name") ? self["nick_or_name"] : nickname || name
end

# sorry. no creativity on this one (just copied nick_or_name)
def name_no_group
has_attribute?("name_no_group") ? self["name_no_group"] : nickname || name
end

# a (local) virtual_attribute without a uses, but with arel
virtual_attribute :nick_or_name, :string, :arel => (lambda do |t|
t.grouping(Arel::Nodes::NamedFunction.new('COALESCE', [t[:nickname], t[:name]]))
end)

# We did not support arel returning something other than Grouping.
# this is here to test what happens when we do
virtual_attribute :name_no_group, :string, :arel => (lambda do |t|
Arel::Nodes::NamedFunction.new('COALESCE', [t[:nickname], t[:name]])
end)

def first_book_name
has_attribute?("first_book_name") ? self["first_book_name"] : books.first.name
end) do
nickname || name
end

def first_book_author_name
has_attribute?("first_book_author_name") ? self["first_book_author_name"] : books.first.author_name
# This tests that we still support defining arel lambdas that return a Grouping.
virtual_attribute :name_no_group, :string, :arel => (lambda do |t|
t.grouping(Arel::Nodes::NamedFunction.new('COALESCE', [t[:nickname], t[:name]]))
end) do
nickname || name
end

def upper_first_book_author_name
Expand All @@ -104,9 +91,14 @@ def book_with_most_bookmarks

virtual_has_one :book_with_most_bookmarks, :uses => {:books => :bookmarks}
# attribute using a relation
virtual_attribute :first_book_name, :string, :uses => [:books]
virtual_attribute :first_book_name, :string, :uses => [:books] do
books.first.name
end
# attribute on a double relation
virtual_attribute :first_book_author_name, :string, :uses => {:books => :author_name}
virtual_attribute :first_book_author_name, :string, :uses => {:books => :author_name} do
books.first.author_name
end

# uses another virtual attribute that uses a relation
virtual_attribute :upper_first_book_author_name, :string, :uses => :first_book_author_name
# :uses points to a virtual_attribute that has a :uses with a hash
Expand Down