Skip to content

Read a has_one as an Option - #53

Merged
nz merged 3 commits into
mainfrom
nz/optional-has-one
Aug 12, 2026
Merged

Read a has_one as an Option#53
nz merged 3 commits into
mainfrom
nz/optional-has-one

Conversation

@nz

@nz nz commented Aug 12, 2026

Copy link
Copy Markdown
Member

Second of the stack, on top of #52. nz/nested-attributes-unwrap and nz/cheap-recursion-guard follow.

A has_one carries the same absence a nullable column does

A has_one is absent whenever no row points back at the record, which is the same kind of absence a nullable column carries. Before this, a converted model answered in two dialects: an Option from its columns and its optional: true belongs_to, and a bare nil from its has_one. A caller had to know which reader spoke which, and the compromise the concern documents — None#nil? answers true so ordinary nil checks keep working — quietly papered over the difference until something reached for a combinator.

class Author < ApplicationRecord
  include Errgonomic::Rails::ActiveRecordOptional

  has_one :profile          # Some(profile) or None()
  has_one :owner, required: true   # left alone
end

required: true is the exception. It asserts the record is there, so absence is a validation failure rather than a value for a caller to handle, and the reader stays plain.

Absence being representable does not excuse breaking the association

The wrapped reader has to keep the association's own machinery working, so the tests cover more than reading:

  • reading an absent and a present has_one, declared on either side of the include
  • create_profile! and direct assignment followed by save!
  • dependent: :destroy still destroying the child
  • a required: true has_one staying unwrapped, and still failing validation when its record is missing

accepts_nested_attributes_for on a has_one does not survive this, for the same reason it does not survive on an optional: true belongs_to: ActiveRecord assigns through the public reader and asks the value whether it is a new record. That is a pre-existing defect rather than something this PR introduces, and nz/nested-attributes-unwrap is next in the stack because of it.

Testing

rake — 27 tests, 71 assertions, and 118 doctests, all passing.

nz added 2 commits August 12, 2026 15:11
A has_one is absent whenever no row points back at the record, which is the
same absence a nullable column carries, so a converted model that returned
a bare nil from half its readers made the surface inconsistent: a caller
had to know which kind of absence each reader spoke.

required: true is left alone. It asserts the record is there, so absence is
a validation failure rather than a value to handle.
accepts_nested_attributes_for assigns through the public reader and asks
whatever it finds there whether it is a new record. A None answers nil? but
is not nil, so it reached that question and raised, taking out build,
update and destroy for the association — on an optional belongs_to as much
as on a has_one.

ActiveRecord needs a bare nil here, the way it needs the raw value behind
encrypts, so this takes the same shape: the association is recorded as an
exclusion and keeps its plain reader.
Leave a singular association with nested attributes unwrapped
@nz
nz merged commit f321893 into main Aug 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants