diff --git a/README.md b/README.md index 20f3a2e..94f113a 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,6 @@ Roughly in order of priority. - [x] Index caching - [x] Mapped Range Indexes - [x] Hierarchical multi index +- [x] Ecto Transactions - [ ] FDB Watches -- [ ] Ecto Transactions - [ ] Logging for all Adapter behaviours -- [ ] Composite primary key diff --git a/lib/ecto/adapters/foundationdb.ex b/lib/ecto/adapters/foundationdb.ex index ff7eb85..007c748 100644 --- a/lib/ecto/adapters/foundationdb.ex +++ b/lib/ecto/adapters/foundationdb.ex @@ -112,21 +112,36 @@ defmodule Ecto.Adapters.FoundationDB do ### Transactions - `ecto_foundationdb` implements its own transaction API. This was decided early on - to make sure we can support tenants and custom indexes. A transaction always - executes on a single tenant, and so individual Repo calls inside your - transaction do not need to specify a `:prefix`. + `ecto_foundationdb` exposes the Ecto.Repo transaction function, and it also exposes + a FoundationDB-specific transaction. Typically you will use the Ecto.Repo transaction. + + ```elixir + TestRepo.transaction(fn -> + # Ecto work + end, prefix: tenant) + ``` + + ```elixir + FoundationDB.transactional(tenant, + fn tx -> + # :erlfdb work + end) + ``` ### Migrations At first glance, `:ecto_foundationdb` migrations may look similar to that of `:ecto_sql`, but the actual execution of migrations and how your app must be configured are very - differently, so please read this section in full. + different, so please read this section in full. If your app uses indexes on any of your schemas, you must define a `:migrator` option on your repo that is a module implementing the `Ecto.Adapters.FoundationDB.Migrator` behaviour. + Migrations are only used for index management. There are no tables to add, drop, or modify. + When you add a field to your Schema, any read requests on old objects will return `nil` in + that field. Right now there is no way to rename a field. + As tenants are opened during your application runtime, migrations will be executed automatically. This distributes the migration across a potentially long period of time, as migrations will not be executed unless the tenant is opened. @@ -204,6 +219,7 @@ defmodule Ecto.Adapters.FoundationDB do @behaviour Ecto.Adapter.Storage @behaviour Ecto.Adapter.Schema @behaviour Ecto.Adapter.Queryable + @behaviour Ecto.Adapter.Transaction @behaviour Ecto.Adapters.FoundationDB.Migration alias Ecto.Adapters.FoundationDB.Database @@ -212,6 +228,7 @@ defmodule Ecto.Adapters.FoundationDB do alias Ecto.Adapters.FoundationDB.EctoAdapterQueryable alias Ecto.Adapters.FoundationDB.EctoAdapterSchema alias Ecto.Adapters.FoundationDB.EctoAdapterStorage + alias Ecto.Adapters.FoundationDB.EctoAdapterTransaction alias Ecto.Adapters.FoundationDB.Layer.Tx alias Ecto.Adapters.FoundationDB.Options alias Ecto.Adapters.FoundationDB.Tenant @@ -325,6 +342,15 @@ defmodule Ecto.Adapters.FoundationDB do defdelegate stream(adapter_meta, query_meta, query_cache, params, options), to: EctoAdapterQueryable + @impl Ecto.Adapter.Transaction + defdelegate transaction(adapter_meta, options, function), to: EctoAdapterTransaction + + @impl Ecto.Adapter.Transaction + defdelegate in_transaction?(adapter_meta), to: EctoAdapterTransaction + + @impl Ecto.Adapter.Transaction + defdelegate rollback(adapter_meta, value), to: EctoAdapterTransaction + @impl Ecto.Adapters.FoundationDB.Migration defdelegate supports_ddl_transaction?(), to: EctoAdapterMigration diff --git a/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex b/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex new file mode 100644 index 0000000..a84096c --- /dev/null +++ b/lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex @@ -0,0 +1,56 @@ +defmodule Ecto.Adapters.FoundationDB.EctoAdapterTransaction do + @moduledoc """ + Implemenation of Ecto.Adapter.Transaction + """ + alias Ecto.Adapters.FoundationDB + alias Ecto.Adapters.FoundationDB.Layer.Tx + @behaviour Ecto.Adapter.Transaction + + @rollback :__ectofdbtxrollback__ + + @doc """ + Runs the given function inside a transaction. + + Returns `{:ok, value}` if the transaction was successful where `value` + is the value return by the function or `{:error, value}` if the transaction + was rolled back where `value` is the value given to `rollback/1`. + """ + @impl true + def transaction(_adapter_meta, options, function) when is_function(function, 0) do + FoundationDB.transactional(options[:prefix], fn -> + function.() + end) + catch + {@rollback, value} -> {:error, value} + end + + def transaction(_adapter_meta, options, function) when is_function(function, 1) do + FoundationDB.transactional(options[:prefix], fn repo -> + function.(repo) + end) + catch + {:__ectofdbtxrollback__, value} -> {:error, value} + end + + @doc """ + Returns true if the given process is inside a transaction. + """ + @impl true + def in_transaction?(_adapter_meta) do + Tx.in_tx?() + end + + @doc """ + Rolls back the current transaction. + + The transaction will return the value given as `{:error, value}`. + + See `c:Ecto.Repo.rollback/1`. + """ + @impl true + def rollback(adapter_meta, value) do + if in_transaction?(adapter_meta) do + throw({@rollback, value}) + end + end +end diff --git a/lib/ecto/adapters/foundationdb/layer.ex b/lib/ecto/adapters/foundationdb/layer.ex index 4f9cf6e..dcb1538 100644 --- a/lib/ecto/adapters/foundationdb/layer.ex +++ b/lib/ecto/adapters/foundationdb/layer.ex @@ -155,10 +155,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer do ...> Repo.insert!(%User{name: "John"}) ...> Repo.insert!(%Event{timestamp: ~N[2024-02-18 12:34:56], data: "Welcome John"}) ...> end - iex> FoundationDB.transactional(tenant, fun) - - For now, this Transaction lives separate from Ecto's own Transaction, so please be mindful when using - this feature to use `Ecto.Adapters.FoundationDB.transactional`. + iex> Repo.transaction(fun, prefix: tenant) """ end diff --git a/lib/ecto/adapters/foundationdb/migrator.ex b/lib/ecto/adapters/foundationdb/migrator.ex index 80523f1..33969ea 100644 --- a/lib/ecto/adapters/foundationdb/migrator.ex +++ b/lib/ecto/adapters/foundationdb/migrator.ex @@ -106,9 +106,7 @@ defmodule Ecto.Adapters.FoundationDB.Migrator do opts |> Keyword.put(:log, migrator_log(opts)) - FoundationDB.transactional(tenant, fn -> - tx_do_up(repo, config, version, module, opts) - end) + repo.transaction(fn -> tx_do_up(repo, config, version, module, opts) end, prefix: tenant) end def tx_do_up(repo, config, version, module, opts) do diff --git a/test/ecto/integration/crud_test.exs b/test/ecto/integration/crud_test.exs index 799b707..0d456eb 100644 --- a/test/ecto/integration/crud_test.exs +++ b/test/ecto/integration/crud_test.exs @@ -79,18 +79,17 @@ defmodule Ecto.Integration.CrudTest do # Crossing a struct into another tenant is not allowed when using a transaction. assert_raise(IncorrectTenancy, ~r/original transaction context .* did not match/, fn -> - FoundationDB.transactional( - other_tenant, + TestRepo.transaction( fn -> TestRepo.insert(user) - end + end, + prefix: other_tenant ) end) # Here are 2 ways to force a struct into a different tenant's transaction assert :ok = - FoundationDB.transactional( - other_tenant, + TestRepo.transaction( fn -> # Specify the equivalent tenant %User{user | id: nil} @@ -103,7 +102,8 @@ defmodule Ecto.Integration.CrudTest do |> TestRepo.insert() :ok - end + end, + prefix: other_tenant ) end @@ -201,8 +201,7 @@ defmodule Ecto.Integration.CrudTest do # Operations inside a FoundationDB Adapater Transaction have the tenant applied # automatically. user = - FoundationDB.transactional( - tenant, + TestRepo.transaction( fn -> {:ok, jesse} = %User{name: "Jesse"} @@ -213,7 +212,8 @@ defmodule Ecto.Integration.CrudTest do |> TestRepo.insert() TestRepo.get(User, jesse.id) - end + end, + prefix: tenant ) assert user.name == "Jesse" @@ -224,13 +224,13 @@ defmodule Ecto.Integration.CrudTest do names = ~w/John James Jesse Sarah Bob Steve/ - FoundationDB.transactional( - tenant, + TestRepo.transaction( fn -> for n <- names do TestRepo.insert(%User{name: n}) end - end + end, + prefix: tenant ) # Each chunk of the stream is retrieved in a separate FDB transaction