From 16abc011ae90d5ecb5ec863db0df52ac34d9a7f7 Mon Sep 17 00:00:00 2001 From: Malte Rohde Date: Sat, 1 Aug 2026 14:16:26 +0200 Subject: [PATCH] Add more lock_modes to Repo.fetch_by/3 --- CHANGELOG.md | 5 +++-- lib/bitcrowd_ecto/repo.ex | 18 ++++++++++++++++-- test/bitcrowd_ecto/repo_test.exs | 23 +++++++++++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4759aa2..b9bb093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ### Added -# [1.1.1] - 2026-01-27 +* Added lock modes `:share` and `:key_share` to `Repo.fetch_by/3` as well as the ability to pass a function. + +## [1.1.1] - 2026-01-27 ### Fixed @@ -22,7 +24,6 @@ * Ensure field/assoc/embed exists when listing errors in `flat_errors_on/3`. This prevents accidental test passes on typos in assertions like `refute_errors_on(cs, :sommtypo)`. - ## [1.0.0] - 2023-12-21 No changes from v0.17.0. diff --git a/lib/bitcrowd_ecto/repo.ex b/lib/bitcrowd_ecto/repo.ex index 519f51d..f389f58 100644 --- a/lib/bitcrowd_ecto/repo.ex +++ b/lib/bitcrowd_ecto/repo.ex @@ -18,7 +18,12 @@ defmodule BitcrowdEcto.Repo do import Ecto.Query, only: [lock: 2, preload: 2, where: 3] alias Ecto.Adapters.SQL - @type lock_mode :: :no_key_update | :update + @type lock_mode :: + :key_share + | :share + | :no_key_update + | :update + | (Ecto.Queryable.t() -> Ecto.Queryable.t()) @type fetch_option :: {:lock, lock_mode | false} @@ -94,7 +99,7 @@ defmodule BitcrowdEcto.Repo do ## Options - * `lock` any of `[:no_key_update, :update]` (defaults to `false`) + * `lock` any of `[:key_share, :share, :no_key_update, :update]` or a function (defaults to `false`) * `preload` allows to preload associations * `error_tag` allows to specify a custom "tag" value (instead of the queryable) or `false` to disabled tagged error tuples @@ -238,12 +243,21 @@ defmodule BitcrowdEcto.Repo do defp maybe_apply_lock(queryable, opts) do case Keyword.get(opts, :lock, false) do + :key_share -> + lock(queryable, "FOR KEY SHARE") + + :share -> + lock(queryable, "FOR SHARE") + :no_key_update -> lock(queryable, "FOR NO KEY UPDATE") :update -> lock(queryable, "FOR UPDATE") + fun when is_function(fun) -> + fun.(queryable) + disabled when disabled in [nil, false] -> queryable diff --git a/test/bitcrowd_ecto/repo_test.exs b/test/bitcrowd_ecto/repo_test.exs index b0570e3..5d6535c 100644 --- a/test/bitcrowd_ecto/repo_test.exs +++ b/test/bitcrowd_ecto/repo_test.exs @@ -2,7 +2,7 @@ defmodule BitcrowdEcto.RepoTest do use BitcrowdEcto.TestCase, async: true - require Ecto.Query + import Ecto.Query alias BitcrowdEcto.TestRepoWithUntaggedNotFoundErrors defp insert_test_schema(_) do @@ -129,6 +129,25 @@ defmodule BitcrowdEcto.RepoTest do end) end + test "can lock for :share", %{resource: %{id: id} = resource} do + assert_lock_granted("relation = 'test_schema_pkey'::regclass::oid", fn -> + assert TestRepo.fetch_by(TestSchema, [id: id], lock: :share) == {:ok, resource} + end) + end + + test "can lock for :key_share", %{resource: %{id: id} = resource} do + assert_lock_granted("relation = 'test_schema_pkey'::regclass::oid", fn -> + assert TestRepo.fetch_by(TestSchema, [id: id], lock: :key_share) == {:ok, resource} + end) + end + + test "can lock with a function", %{resource: %{id: id} = resource} do + assert_lock_granted("relation = 'test_schema_pkey'::regclass::oid", fn -> + assert TestRepo.fetch_by(TestSchema, [id: id], lock: &lock(&1, "FOR SHARE")) == + {:ok, resource} + end) + end + test "converts CastErrors for binary_id columns to not_found errors" do assert TestRepo.fetch_by(TestSchema, some_uuid: "doesnotcast") == {:error, {:not_found, TestSchema}} @@ -147,7 +166,7 @@ defmodule BitcrowdEcto.RepoTest do end test "returns the given error tag instead of the queryable" do - query = Ecto.Query.from(x in TestSchema, where: x.id == ^Ecto.UUID.generate()) + query = from(x in TestSchema, where: x.id == ^Ecto.UUID.generate()) assert TestRepo.fetch_by(query, []) == {:error, {:not_found, query}} assert TestRepo.fetch_by(query, [], error_tag: :foo) == {:error, {:not_found, :foo}} end