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
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