Add Sequel sole plugin; drop User.upsert
sole provides a single-record assertion at the dataset level,
avoiding silent bugs from first/last when exactly one row is
expected. Upsert removed—name sync on every request was
unnecessary overhead with no current consumer.

Assisted-by: Claude Opus 4.6 via pi
change rlssunuskuslrsvnrkrvyysroyxqpxkz
commit 817479f124ea08e38ff9036435d86703d778c8bd
author Alpha Chen <alpha@kejadlen.dev>
date
parent tpnrumsy
diff --git a/lib/models.rb b/lib/models.rb
index 6cf1a5a..daa3473 100644
--- a/lib/models.rb
+++ b/lib/models.rb
@@ -3,15 +3,10 @@
 require_relative "db"
 
 Sequel::Model.plugin :timestamps, update_on_create: true
+Sequel::Model.plugin :sole
 
 class User < Sequel::Model
   one_to_many :series
-
-  def self.upsert(login:, name:)
-    user = find_or_create(login: login) { |u| u.name = name }
-    user.update(name: name) if user.name != name
-    user
-  end
 end
 
 class Series < Sequel::Model
diff --git a/lib/sequel/plugins/sole.rb b/lib/sequel/plugins/sole.rb
new file mode 100644
index 0000000..2765190
--- /dev/null
+++ b/lib/sequel/plugins/sole.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Sequel
+  module Plugins
+    # The sole plugin adds a +sole+ dataset method that returns the
+    # single matching record, raising if zero or more than one record
+    # matches.
+    #
+    #   User.where(login: "alice").sole  # => #<User ...>
+    #   User.where(login: "nobody").sole # raises Sequel::NoMatchingRow
+    #   User.dataset.sole               # raises Sequel::Plugins::Sole::TooManyRows (if > 1)
+    module Sole
+      class TooManyRows < Sequel::Error; end
+
+      module DatasetMethods
+        def sole
+          results = limit(2).all
+          raise Sequel::NoMatchingRow.new(self) if results.empty?
+          raise TooManyRows, "expected 1 row, got multiple" if results.length > 1
+
+          results.first
+        end
+      end
+    end
+  end
+end
diff --git a/lib/web.rb b/lib/web.rb
index 28fe64d..d93a64c 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -14,7 +14,7 @@ class Web < Roda
     return unless login
 
     name = env["HTTP_TAILSCALE_USER_NAME"]
-    User.upsert(login: login, name: name)
+    User.find_or_create(login: login) { |u| u.name = name }
   end
 
   route do |r|
diff --git a/test/test_sole.rb b/test/test_sole.rb
new file mode 100644
index 0000000..82a350d
--- /dev/null
+++ b/test/test_sole.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+ENV["DATABASE_URL"] = ":memory:"
+
+require "minitest/autorun"
+
+require_relative "../lib/web"
+
+class TestSole < Minitest::Test
+  def setup
+    DB[:tasks].delete
+    DB[:series].delete
+    DB[:users].delete
+  end
+
+  def test_sole_returns_single_record
+    User.create(login: "alice@example.com", name: "Alice")
+    user = User.where(login: "alice@example.com").sole
+    assert_equal "Alice", user.name
+  end
+
+  def test_sole_raises_on_no_records
+    assert_raises(Sequel::NoMatchingRow) do
+      User.where(login: "nobody@example.com").sole
+    end
+  end
+
+  def test_sole_raises_on_multiple_records
+    User.create(login: "alice@example.com", name: "Alice")
+    User.create(login: "bob@example.com", name: "Bob")
+
+    assert_raises(Sequel::Plugins::Sole::TooManyRows) do
+      User.dataset.sole
+    end
+  end
+end
diff --git a/test/test_web.rb b/test/test_web.rb
index 4e94998..f732903 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -71,13 +71,6 @@ class TestWeb < Minitest::Test
     assert_equal "Bob", user[:name]
   end
 
-  def test_root_updates_user_name
-    get "/", {}, tailscale_headers(login: "carol@example.com", name: "Carol")
-    get "/", {}, tailscale_headers(login: "carol@example.com", name: "Carol C.")
-    user = DB[:users].first(login: "carol@example.com")
-    assert_equal "Carol C.", user[:name]
-  end
-
   def test_root_requires_tailscale_user
     get "/"
     assert_equal 403, last_response.status