Accepts_nested_attributes_for tries to create before updating existing, validation fails

I’ve got a child model that has an end_date attribute and a validation which ensures only one record can leave this attribute blank (this is also enforced on the database level). When I have a form that allows updating all existing records as well as creating a new one this validation fails.

class Parent < ApplicationRecord
  has_many :children, -> {order("end_date")}, dependent: :destroy
  accepts_nested_attributes_for :children, reject_if: :all_blank, allow_destroy: true
end
class Child < ApplicationRecord
  belongs_to :parent
  validates :end_date, uniqueness: {scope: [:parent]}, unless: :end_date?
end
class Admin::ChildrenController < ApplicationController
  before_action :select_parent

  def edit
  end

  def update
    @parent.update(parent_params)
    unless @parent.errors.any? 
      redirect_to some_path(@parent), notice: t(".success")
    else
      flash.now[:alert] = @parent.errors.full_messages[0]
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def load_parent
    @parent = Parents.find(params[:id])
  end

  def parent_params
    params.expect(parent: [
      children_attributes: [[:id, :start_date, :end_date, :_destroy]]
    ])
  end
end
Processing by ChildrenController#update as HTML
  Parameters: {"authenticity_token"=>"[FILTERED]", "parent"=>{
    "children_attributes"=>{
      "0"=>{"_destroy"=>"0", "start_date"=>"2024-03-06", "end_date"=>"2024-05-10", "id"=>"28213"}, 
      "1"=>{"_destroy"=>"0", "start_date"=>"2024-05-10", "end_date"=>"2024-07-18", "id"=>"28214"}, 
      "2"=>{"_destroy"=>"0", "start_date"=>"2024-07-18", "end_date"=>"2024-08-21", "id"=>"28215"}, 
      "3"=>{"_destroy"=>"0", "start_date"=>"2024-08-21", "end_date"=>"2024-09-01", "id"=>"28216"}, 
      "4"=>{"_destroy"=>"0", "start_date"=>"2024-09-01", "end_date"=>"2024-10-20", "id"=>"28217"}, 
      "5"=>{"_destroy"=>"0", "start_date"=>"2024-10-20", "end_date"=>"2025-01-01", "id"=>"28218"}, 
      "6"=>{"start_date"=>"2025-01-01"}
    }
  }, "commit"=>"Update", "id"=>"12345"}
@parent.valid? 
> false
@parent.errors.full_messages
> ["Only one record without an end_date is allowed"]

If I do not try to create a new child the update is successful. It appears that Rails is trying to create the new record (for which end_date is blank) before updating the existing records (in which the previous one with a blank end_date should now have this set), triggering the “only one record without an end_date is allowed” validation error.

I’m not sure what the “Rails way” of dealing with such a Catch-22 could be?

I’m now leaning towards dropping end_date altogether, since it will always be the same as the start_date of the next child (or null). I’m hoping to replace it with a virtual boolean column “current” instead, indicating the last child (as ordered by start_date), which is updated by a trigger. Apart from getting rid of largely redundant data this has two main benefits: less complexity in the application layer, and the ability to index children on current.

I dropped the end_date but couldn’t find a nice way to get the same conveniences from start_date (e.g. a scope where end_date = nil on parent scope to get the parent object’s “current” child object), nor could I figure out how to set up a DB trigger to provide an automagic “current” column, so I ended up adding a boolean “current” attribute and bodging together a callback on the child model to set it:

  self.implicit_order_column = "start_date"

  after_save :set_current
  after_destroy :set_current

  validates :current, uniqueness: {scope: :parent}, if: :current?

  def set_current
    self.parent.children.update_all(current: false)
    self.parent.children.last.update_column(:current, true)
  end

I don’t like it, but it works.

Have you tried removing the Rails validation and validating at DB level, then catching the error?