Current section
20 Versions
Jump to
Current section
20 Versions
Compare versions
36
files changed
+1471
additions
-730
deletions
| @@ -1,5 +1,25 @@ | |
| 1 1 | # Changelog for v0.x |
| 2 2 | |
| 3 | + ## v0.3 (2024-10-20) |
| 4 | + |
| 5 | + ### \*\* Major breaking changes \*\* |
| 6 | + |
| 7 | + Databases that have been created using a prior version of EctoFoundationDB will be broken on |
| 8 | + EctoFDB v0.3 and above. Please start a new database with EctoFDB v0.3. If you currently have |
| 9 | + a database on v0.2 or earlier, please [submit an issue](https://github.com/foundationdb-beam/ecto_foundationdb/issues) |
| 10 | + to discuss the upgrade path. |
| 11 | + |
| 12 | + ### Enhancements |
| 13 | + |
| 14 | + * [Watches](Ecto.Adapters.FoundationDB.html#module-watches): Support for FDB Watches, which is like a database-driven PubSub on an Ecto struct. |
| 15 | + * [Directory Tenants](EctoFoundationDB.Tenant): A new default backend for Multitenancy that is production-ready. Managed tenants have been moved to "Experimental" status. |
| 16 | + * `@schema_context usetenant: true` is no longer required. |
| 17 | + * The `:open_db` option now defines a 1-arity function that accepts the Repo module. |
| 18 | + |
| 19 | + ### New Documentation |
| 20 | + |
| 21 | + * [Livebook | Watches in LiveView](watches.livemd) |
| 22 | + |
| 3 23 | ## v0.2.1 (2024-09-23) |
| 4 24 | |
| 5 25 | ### Bug fixes |
| @@ -12,8 +12,8 @@ as the driver for communicating with FoundationDB. | |
| 12 12 | Install the latest stable release of FoundationDB from the |
| 13 13 | [official FoundationDB Releases](https://github.com/apple/foundationdb/releases). |
| 14 14 | |
| 15 | - You will only need to install the `foundationdb-server` package if you're |
| 16 | - running an instance of the FoundationDB Server. For example, it's common to |
| 15 | + The `foundationdb-server` package is required on any system that will be running |
| 16 | + a FoundationDB server instance. For example, it's common to |
| 17 17 | run the `foundationdb-server` on your development machine and on managed |
| 18 18 | instances running a FoundationDB cluster, but not for your stateless Elixir |
| 19 19 | application server in production. |
| @@ -25,7 +25,7 @@ Include `:ecto_foundationdb` in your list of dependencies in `mix.exs`: | |
| 25 25 | ```elixir |
| 26 26 | defp deps do |
| 27 27 | [ |
| 28 | - {:ecto_foundationdb, git: "https://github.com/foundationdb-beam/ecto_foundationdb.git", branch: "main"} |
| 28 | + {:ecto_foundationdb, "~> 0.3"} |
| 29 29 | ] |
| 30 30 | end |
| 31 31 | ``` |
| @@ -53,14 +53,7 @@ EctoFoundationDB offers: | |
| 53 53 | See the [documentation](https://hexdocs.pm/ecto_foundationdb) for usage |
| 54 54 | information. |
| 55 55 | |
| 56 | - ### Tenants |
| 57 | - |
| 58 | - `ecto_foundationdb` requires the use of FoundationDB Tenants, which can be |
| 59 | - enabled on your cluster with the following configuration in an `fdbcli` prompt. |
| 60 | - |
| 61 | - ``` |
| 62 | - fdb> configure tenant_mode=optional_experimental |
| 63 | - ``` |
| 56 | + For documentation on `main` branch, see [Ecto.Adapters.FoundationDB](https://github.com/foundationdb-beam/ecto_foundationdb/blob/main/lib/ecto/adapters/foundationdb.ex). |
| 64 57 | |
| 65 58 | ## Running tests |
| @@ -2,7 +2,7 @@ | |
| 2 2 | [{<<"GitHub">>, |
| 3 3 | <<"https://github.com/foundationdb-beam/ecto_foundationdb">>}]}. |
| 4 4 | {<<"name">>,<<"ecto_foundationdb">>}. |
| 5 | - {<<"version">>,<<"0.2.1">>}. |
| 5 | + {<<"version">>,<<"0.3.0">>}. |
| 6 6 | {<<"description">>,<<"FoundationDB adapter for Ecto">>}. |
| 7 7 | {<<"elixir">>,<<"~> 1.15">>}. |
| 8 8 | {<<"app">>,<<"ecto_foundationdb">>}. |
| @@ -26,6 +26,10 @@ | |
| 26 26 | {<<"files">>, |
| 27 27 | [<<"lib">>,<<"lib/ecto_foundationdb">>, |
| 28 28 | <<"lib/ecto_foundationdb/query_plan.ex">>, |
| 29 | + <<"lib/ecto_foundationdb/tenant">>, |
| 30 | + <<"lib/ecto_foundationdb/tenant/backend.ex">>, |
| 31 | + <<"lib/ecto_foundationdb/tenant/directory_tenant.ex">>, |
| 32 | + <<"lib/ecto_foundationdb/tenant/managed_tenant.ex">>, |
| 29 33 | <<"lib/ecto_foundationdb/database.ex">>, |
| 30 34 | <<"lib/ecto_foundationdb/options.ex">>, |
| 31 35 | <<"lib/ecto_foundationdb/tenant.ex">>, |
| @@ -40,7 +44,8 @@ | |
| 40 44 | <<"lib/ecto_foundationdb/layer/query.ex">>, |
| 41 45 | <<"lib/ecto_foundationdb/layer/tx.ex">>, |
| 42 46 | <<"lib/ecto_foundationdb/layer/ordering.ex">>, |
| 43 | - <<"lib/ecto_foundationdb/migrator.ex">>, |
| 47 | + <<"lib/ecto_foundationdb/migrator.ex">>,<<"lib/ecto_foundationdb/sandbox">>, |
| 48 | + <<"lib/ecto_foundationdb/sandbox/sandboxer.ex">>, |
| 44 49 | <<"lib/ecto_foundationdb/schema.ex">>, |
| 45 50 | <<"lib/ecto_foundationdb/schema_migration.ex">>, |
| 46 51 | <<"lib/ecto_foundationdb/sandbox.ex">>, |
| @@ -55,8 +60,9 @@ | |
| 55 60 | <<"lib/ecto/adapters/foundationdb.ex">>, |
| 56 61 | <<"lib/ecto/adapters/foundationdb">>, |
| 57 62 | <<"lib/ecto/adapters/foundationdb/supervisor.ex">>, |
| 58 | - <<"lib/ecto/adapters/foundationdb/ecto_adapter_storage.ex">>, |
| 59 63 | <<"lib/ecto/adapters/foundationdb/ecto_adapter_schema.ex">>, |
| 64 | + <<"lib/ecto/adapters/foundationdb/ecto_adapter_async.ex">>, |
| 65 | + <<"lib/ecto/adapters/foundationdb/ecto_adapter_assigns.ex">>, |
| 60 66 | <<"lib/ecto/adapters/foundationdb/ecto_adapter_transaction.ex">>, |
| 61 67 | <<"lib/ecto/adapters/foundationdb/ecto_adapter_queryable.ex">>, |
| 62 68 | <<"lib/ecto/adapters/foundationdb/ecto_adapter.ex">>,<<".formatter.exs">>, |
| @@ -3,9 +3,10 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 3 3 | |
| 4 4 | ([Hex.pm](https://hex.pm/packages/ecto_foundationdb) | [GitHub](https://github.com/foundationdb-beam/ecto_foundationdb)) |
| 5 5 | |
| 6 | - Adapter module for FoundationDB. |
| 6 | + Adapter module for [FoundationDB](https://www.foundationdb.org). |
| 7 7 | |
| 8 | - It uses `:erlfdb` for communicating with the database. |
| 8 | + It uses [erlfdb](https://github.com/foundationdb-beam/erlfdb) for communicating with the database, |
| 9 | + and implements an Ecto-compatible Layer on top of FDB's key-value store. |
| 9 10 | |
| 10 11 | ## Installation |
| 11 12 | |
| @@ -25,7 +26,7 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 25 26 | ```elixir |
| 26 27 | defp deps do |
| 27 28 | [ |
| 28 | - {:ecto_foundationdb, "~> 0.1"} |
| 29 | + {:ecto_foundationdb, "~> 0.3"} |
| 29 30 | ] |
| 30 31 | end |
| 31 32 | ``` |
| @@ -62,33 +63,36 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 62 63 | Supervisor.start_link(...) |
| 63 64 | ``` |
| 64 65 | |
| 65 | - ## Tenants |
| 66 | + ### Tenants |
| 66 67 | |
| 67 | - EctoFoundationDB requires the use of [FoundationDB Tenants](https://apple.github.io/foundationdb/tenants.html). |
| 68 | + EctoFoundationDB requires your application to use multitenancy via `EctoFoundationDB.Tenant`. |
| 69 | + We strongly encourage your application to decide on a multitenancy strategy that makes sense |
| 70 | + for your problem domain (or simply use `"default"` to get started). |
| 68 71 | |
| 69 | - Each tenant you create has a separate keyspace from all others, and a given FoundationDB |
| 70 | - Transaction is guaranteed to be isolated to a particular tenant's keyspace. |
| 72 | + Once you have opened a tenant, you'll use the Ecto `:prefix` option to provide the tenant |
| 73 | + to each Repo operation. |
| 71 74 | |
| 72 | - You'll use the Ecto `:prefix` option to specify the relevant tenant for each Ecto operation |
| 73 | - in your application. |
| 75 | + ### Creating a schema |
| 74 76 | |
| 75 | - Creating a schema to be used in a tenant. |
| 77 | + This is a standard `Ecto.Schema` that will be used in the examples of this documentation. |
| 76 78 | |
| 77 79 | ```elixir |
| 78 80 | defmodule User do |
| 79 81 | use Ecto.Schema |
| 80 | - @schema_context usetenant: true |
| 81 82 | @primary_key {:id, :binary_id, autogenerate: true} |
| 82 83 | schema "users" do |
| 83 84 | field(:name, :string) |
| 84 85 | field(:department, :string) |
| 86 | + field(:balance, :integer) |
| 85 87 | timestamps() |
| 86 88 | end |
| 87 89 | # ... |
| 88 90 | end |
| 89 91 | ``` |
| 90 92 | |
| 91 | - Setting up a tenant. |
| 93 | + ### Opening a tenant |
| 94 | + |
| 95 | + See `EctoFoundationDB.Tenant` for more details. |
| 92 96 | |
| 93 97 | ```elixir |
| 94 98 | alias EctoFoundationDB.Tenant |
| @@ -96,26 +100,46 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 96 100 | tenant = Tenant.open!(MyApp.Repo, "some-org") |
| 97 101 | ``` |
| 98 102 | |
| 99 | - Inserting a new struct. |
| 103 | + ### Inserting a new struct |
| 104 | + |
| 105 | + The Tenant is stored on the struct as metadata, so that when passing structs |
| 106 | + to Repo functions, the `:prefix` option is not required. |
| 100 107 | |
| 101 108 | ```elixir |
| 102 | - user = %User{name: "John", department: "Engineering"} |
| 109 | + alice = %User{name: "Alice", department: "Engineering"} |
| 103 110 | |> FoundationDB.usetenant(tenant) |
| 104 111 | |
| 105 | - user = MyApp.Repo.insert!(user) |
| 112 | + alice = MyApp.Repo.insert!(alice) |
| 106 113 | ``` |
| 107 114 | |
| 108 | - Querying for a struct using the primary key. |
| 115 | + ### Querying for a struct using the primary key |
| 116 | + |
| 117 | + The `:prefix` option is required. The struct returned will have the tenant |
| 118 | + in the metadata, like above. |
| 109 119 | |
| 110 120 | ```elixir |
| 111 | - MyApp.Repo.get!(User, user.id, prefix: tenant) |
| 121 | + MyApp.Repo.get!(User, alice.id, prefix: tenant) |
| 112 122 | ``` |
| 113 123 | |
| 114 | - When a struct is retrieved from a tenant (using `:prefix`), that struct's metadata |
| 115 | - holds onto the tenant reference. This helps to protect your application from a |
| 116 | - struct accidentally crossing tenant boundaries due to some unforeseen bug. |
| 124 | + ### Transactions |
| 117 125 | |
| 118 | - ## Indexes |
| 126 | + Multiple calls to Repo functions can be grouped inside a single transaction, which is |
| 127 | + [ACID](https://en.wikipedia.org/wiki/ACID) and [globally serializable](https://en.wikipedia.org/wiki/Global_serializability). |
| 128 | + |
| 129 | + Here's a simple example. Transactions can be arbitrarily complex, but there is a [limit to the size and runtime](#module-foundationdb). |
| 130 | + |
| 131 | + ```elixir |
| 132 | + MyApp.Repo.transaction(fn -> |
| 133 | + alice = MyApp.Repo.get!(User, alice.id) |
| 134 | + if alice.balance > 0 do |
| 135 | + MyApp.Repo.update(User.changeset(alice, %{balance: alice.balance - 1})) |
| 136 | + else |
| 137 | + raise "Overdraft" |
| 138 | + end |
| 139 | + end, prefix: tenant) |
| 140 | + ``` |
| 141 | + |
| 142 | + ## Indexes and Queries |
| 119 143 | |
| 120 144 | The implication of the [FoundationDB Layer Concept](https://apple.github.io/foundationdb/layer-concept.html) |
| 121 145 | is that the manner in which data can be stored and accessed is the responsibility of the client |
| @@ -141,49 +165,88 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 141 165 | create index(User, [:birthyear, :department, :birthdate]) |
| 142 166 | ``` |
| 143 167 | |
| 144 | - In fact, the index value can be any Elixir term. All types support Equal queries. However, |
| 145 | - certain Ecto Schema types support Between queries. See the Data Types section for the list. |
| 168 | + In fact, the index value can be any Elixir term. All types support Equal queries (of the form `x.field == "value"`). |
| 169 | + However, certain Ecto Schema types support Between queries (of the form `x.field >= 10 and x.field < 20`). |
| 170 | + See the [Data Types](#module-data-types) section for the list. |
| 146 171 | |
| 147 | - For example, When an index is created on timestamp-like fields, it is an effective time |
| 148 | - series index. See the query examples below. |
| 149 | - |
| 150 | - See the Migrations section for further details about managing indexes in your application. |
| 172 | + See the [Migrations](#module-migrations) section for further details about managing indexes in your application. |
| 151 173 | |
| 152 174 | ### Index Query Examples |
| 153 175 | |
| 154 176 | Retrieve all users in the Engineering department (with an Equal constraint): |
| 155 177 | |
| 156 178 | ```elixir |
| 157 | - iex> query = from(u in User, where: u.department == ^"Engineering") |
| 158 | - iex> MyApp.Repo.all(query, prefix: tenant) |
| 179 | + query = from(u in User, where: u.department == ^"Engineering") |
| 180 | + MyApp.Repo.all(query, prefix: tenant) |
| 159 181 | ``` |
| 160 182 | |
| 161 183 | Retrieve all users with a birthyear from 1992 to 1994 (with a Between constraint): |
| 162 184 | |
| 163 185 | ```elixir |
| 164 | - iex> query = from(u in User, |
| 165 | - ...> where: u.birthyear >= ^1992 and u.birthyear <= ^1994 |
| 166 | - ...> ) |
| 167 | - iex> MyApp.Repo.all(query, prefix: tenant) |
| 186 | + query = from(u in User, |
| 187 | + where: u.birthyear >= ^1992 and u.birthyear <= ^1994 |
| 188 | + ) |
| 189 | + MyApp.Repo.all(query, prefix: tenant) |
| 168 190 | ``` |
| 169 191 | |
| 170 192 | Retrieve all Engineers born in August 1992: |
| 171 193 | |
| 172 194 | ```elixir |
| 173 | - iex> query = from(u in User, |
| 174 | - ...> where: u.birthyear == ^1992 and |
| 175 | - ...> u.department == ^"Engineering" and |
| 176 | - ...> u.birthdate >= ^~D[1992-08-01] and u.birthdate < ^~D[1992-09-01] |
| 177 | - ...> ) |
| 178 | - iex> MyApp.Repo.all(query, prefix: tenant) |
| 195 | + query = from(u in User, |
| 196 | + where: u.birthyear == ^1992 and |
| 197 | + u.department == ^"Engineering" and |
| 198 | + u.birthdate >= ^~D[1992-08-01] and u.birthdate < ^~D[1992-09-01] |
| 199 | + ) |
| 200 | + MyApp.Repo.all(query, prefix: tenant) |
| 179 201 | ``` |
| 180 202 | |
| 181 | - **Order matters!**: When you create an index using multiple fields, the FDB key that stores the index will be extended with |
| 182 | - all the values in the order of your defined index fields. Because FoundationDB stores keys in a well-defined order, |
| 183 | - the order of the fields in your index determines the Between queries you can perform. |
| 203 | + ### Index ordering matters |
| 184 204 | |
| 185 | - There can be 0 or 1 Between clauses, and if one exists, it must correspond to the final constraint in the where clause when |
| 186 | - compared against the order of the index fields. |
| 205 | + When you create an index using multiple fields, the FDB key that stores the index will be extended with |
| 206 | + all the values *in the order of your defined index fields*. Because FoundationDB stores the keys themselves |
| 207 | + in a well-defined order, the order of the fields in your index determines the Between queries you can perform. |
| 208 | + |
| 209 | + > **_RULE:_** For any Query, there can be 0 or 1 Between clauses, and if one exists for a field, then there must not |
| 210 | + exist an Equal clause for ay field that follows, according to the list of fields in the index. Moreover, the Between |
| 211 | + field must be of a [Data Type](#module-data-types) supported for Between queries. |
| 212 | + |
| 213 | + Above, we demonstrated a nontrivial query with 2 Equal constraints and 1 Between constraint. Compare that with an invalid |
| 214 | + query below, which attempts a Between constraint on the `:birthyear` field followed by an Equal constraint on the `:department` |
| 215 | + field. |
| 216 | + |
| 217 | + ```elixir |
| 218 | + # Invalid query! |
| 219 | + from(u in User, |
| 220 | + where: u.birthyear >= ^1992 and u.birthyear < ^1995 and |
| 221 | + u.department == ^"Engineering" |
| 222 | + ) |
| 223 | + ``` |
| 224 | + |
| 225 | + **Why is this query unsupported by EctoFDB?** With the index defined as-is, EctoFDB is unable to extract a minimal |
| 226 | + dataset from the database in one operation. Instead of over-extracting data and then applying a filter, we've chosen |
| 227 | + to reject the query. We hope this encourages correct index management and removes worry about execution time of |
| 228 | + individual Repo function calls. |
| 229 | + |
| 230 | + **Yeah that's cool, but how do I run the query I want to run?** You have 2 options. |
| 231 | + |
| 232 | + 1. Purposefully over-extract the data using Stream, and do the filtering in Elixir: |
| 233 | + |
| 234 | + ```elixir |
| 235 | + from(u in User, where: u.birthyear >= ^1992 and u.birthyear < 1995 and) |
| 236 | + |> MyApp.Repo.stream(prefix: tenant) |
| 237 | + |> Stream.filter(&((&1).department == "Engineering")) |
| 238 | + |> Enum.to_list() |
| 239 | + ``` |
| 240 | + |
| 241 | + or |
| 242 | + |
| 243 | + 2. Create the necessary index. For our example above, the new index might look like: |
| 244 | + |
| 245 | + ```elixir |
| 246 | + create index(User, [:department, :birthyear]) |
| 247 | + ``` |
| 248 | + |
| 249 | + We encourage you to test execution times of your expected workload before deciding to create an index. |
| 187 250 | |
| 188 251 | ### Custom Indexes |
| 189 252 | |
| @@ -205,28 +268,12 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 205 268 | end, prefix: tenant) |
| 206 269 | ``` |
| 207 270 | |
| 208 | - It also exposes a FoundationDB-specific transaction: |
| 209 | - |
| 210 | - ```elixir |
| 211 | - alias Ecto.Adapters.FoundationDB |
| 212 | - |
| 213 | - FoundationDB.transactional(tenant, |
| 214 | - fn tx -> |
| 215 | - # :erlfdb work |
| 216 | - end) |
| 217 | - ``` |
| 218 | - |
| 219 | - Both of these calling conventions create a transaction on FDB. Typically you will use |
| 220 | - `Repo.transaction/1` when operating on your Ecto.Schema structs. If you wish |
| 221 | - to do anything using the lower-level `:erlfdb` API (rare), you will use |
| 222 | - `FoundationDB.transactional/2`. |
| 223 | - |
| 224 | - Please read the [FoundationDB Developer Guid on transactions](https://apple.github.io/foundationdb/developer-guide.html#transaction-basics) |
| 271 | + Please read the [FoundationDB Developer Guide on transactions](https://apple.github.io/foundationdb/developer-guide.html#transaction-basics) |
| 225 272 | for more information about how to develop with transactions. |
| 226 273 | |
| 227 274 | It's important to remember that even though the database gives ACID guarantees about the |
| 228 | - keys updated in a transaction, the Elixir function passed into `Repo.transaction/1` or |
| 229 | - `FoundationDB.transactional/2` will be executed more than once when any conflicts |
| 275 | + keys updated in a transaction, the Elixir function passed into `Repo.transaction/1` |
| 276 | + may be executed more than once when any conflicts |
| 230 277 | are encountered. For this reason, your function must not have any side effects other than |
| 231 278 | the database operations. For example, do not publish any messages to other processes |
| 232 279 | (such as `Phoenix.PubSub`) from within the transaction. |
| @@ -234,7 +281,7 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 234 281 | ```elixir |
| 235 282 | # Do not do this! |
| 236 283 | MyApp.Repo.transaction(fn -> |
| 237 | - MyApp.Repo.insert(%User{name: "John"}) |
| 284 | + MyApp.Repo.insert(%User{name: "Alice"}) |
| 238 285 | ... |
| 239 286 | Phoenix.PubSub.broadcast("my_topic", "new_user") # Not safe! :( |
| 240 287 | end, prefix: tenant) |
| @@ -243,12 +290,15 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 243 290 | ```elixir |
| 244 291 | # Instead, do this: |
| 245 292 | MyApp.Repo.transaction(fn -> |
| 246 | - MyApp.Repo.insert(%User{name: "John"}) |
| 293 | + MyApp.Repo.insert(%User{name: "Alice"}) |
| 247 294 | ... |
| 248 295 | end, prefix: tenant) |
| 249 296 | Phoenix.PubSub.broadcast("my_topic", "new_user") # Safe :) |
| 250 297 | ``` |
| 251 298 | |
| 299 | + If you're looking for PubSub-like functionality at the transaction-level, please see |
| 300 | + [Watches](#module-watches). |
| 301 | + |
| 252 302 | ## Migrations |
| 253 303 | |
| 254 304 | At first glance, EctoFoundationDB migrations may look similar to that of `:ecto_sql`, |
| @@ -265,7 +315,9 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 265 315 | |
| 266 316 | As tenants are opened during your application runtime, migrations will be executed |
| 267 317 | automatically. This distributes the migration across a potentially long period of time, |
| 268 | - as migrations will not be executed unless the tenant is opened. |
| 318 | + as migrations will not be executed unless the tenant is opened. Specifically, a call to |
| 319 | + `Tenant.open/2` will block until the migration for that tenant completes. Other tenants |
| 320 | + will not be affected. |
| 269 321 | |
| 270 322 | The Migrator is a module in your application runtime that provides the full list of |
| 271 323 | ordered migrations. These are the migrations that will be executed when a tenant is opened. |
| @@ -320,7 +372,7 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 320 372 | conversion between Elixir types and Database Types. Any term you put in your |
| 321 373 | struct will be stored and later retrieved. |
| 322 374 | |
| 323 | - Data types are used by EctoFoundationDB for the creation and querying of indexes. |
| 375 | + Data types *are* used by EctoFoundationDB for the creation and querying of indexes. |
| 324 376 | Certain Ecto types are encoded into the FDB key, which allows you to formulate |
| 325 377 | Between queries on indexes of these types: |
| 326 378 | |
| @@ -339,7 +391,95 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 339 391 | - `:utc_datetime` |
| 340 392 | - `:utc_datetime_usec` |
| 341 393 | |
| 342 | - See Queries for more information. |
| 394 | + See [Indexes and Queries](#module-indexes-and-queries) for more information. |
| 395 | + |
| 396 | + ## Watches |
| 397 | + |
| 398 | + [FoundationDB Watches](https://apple.github.io/foundationdb/developer-guide.html#watches) are |
| 399 | + similar to Triggers in an RDBMS. Registering a watch on a particular key provides a guarantee[*](https://github.com/apple/foundationdb/wiki/An-Overview-how-Watches-Work) |
| 400 | + that when that key in the database is changed, the client application is notified with a |
| 401 | + push-style notification. This registration is initiated inside a transaction, and the caller is |
| 402 | + provided a `EctoFoundationDB.Future` object that is used to receive the push notification at a later time. |
| 403 | + The notification is delivered directly to the Elixir process that requested it. |
| 404 | + |
| 405 | + The EctoFDB Repo module allows you to register a watch on any struct that you have written |
| 406 | + to the database. The FDB watch is registered on the key that represents the Primary Write (the key-value |
| 407 | + that stores all the data of your struct). When that struct changes in the database via any means, |
| 408 | + the Future is resolved and the caller has the opportunity to refresh its internal state. |
| 409 | + |
| 410 | + ### EctoFDB Repo functions for watches |
| 411 | + |
| 412 | + EctoFDB adds the following functions to your Repo module. They are each discussed in detail |
| 413 | + below. |
| 414 | + |
| 415 | + - `Repo.watch/1`/`Repo.watch/2`: Registers a watch |
| 416 | + - `Repo.assign_ready/2`/`Repo.assign_ready/3`: Handles Future resolution via a push message |
| 417 | + |
| 418 | + ### `Repo.watch/2` |
| 419 | + |
| 420 | + With arguments `struct` and `options`, registers |
| 421 | + a watch on the given struct and returns a Future. This Future resolves to |
| 422 | + a value when the FDB key is set or cleared. Until then, the Future remains unresolved. |
| 423 | + The Future from a watch is resolvable outside of an FDB transaction. |
| 424 | + |
| 425 | + In addition to other standard options, a `:label` option will prime the Future for use by `assign_ready/3`. |
| 426 | + It's recommended that you provide a label and use `assign_ready/3`, but not required. |
| 427 | + |
| 428 | + ### `Repo.assign_ready/3` |
| 429 | + |
| 430 | + With arguments `futures`, `ready_refs`, and |
| 431 | + `options`, returns a tuple containing: 1) a Keyword of structs with keys according to the `:label` |
| 432 | + provided to `watch/2`, and 2) an updated list of futures that are still unresolved. |
| 433 | + |
| 434 | + In order for the caller to acquire the `ready_refs` arguments, you must receive messages from the |
| 435 | + process mailbox of the form `{ref, :ready}`. See the example below for detail. If your process chooses |
| 436 | + to buffer a list `ready_refs` longer than 1, then `assign_ready/3` uses pipelining to retrieve the |
| 437 | + resulting structs. Each watched struct must have been registered with a unique `:label`. |
| 438 | + |
| 439 | + The `options` are provided to `Repo.async_get/3` internally. In addition to standard options, a |
| 440 | + `watch?: true` option will re-register a new watch on the struct so that your calling |
| 441 | + process's event loop can always be aware of changes. |
| 442 | + |
| 443 | + ### Example |
| 444 | + |
| 445 | + The power of watches is best illustrated in the context of a stateful Elixir process that is |
| 446 | + interested in tracking the up-to-date value for a particular schema. This could be a GenServer, LiveView, |
| 447 | + or some other stateful process. |
| 448 | + |
| 449 | + ```elixir |
| 450 | + defmodule MyAliceView do |
| 451 | + |
| 452 | + # ... |
| 453 | + |
| 454 | + def init(tenant) do |
| 455 | + {alice, future} = MyRepo.transaction( |
| 456 | + fn -> |
| 457 | + alice = MyRepo.get_by(User, name: "Alice") |
| 458 | + {alice, MyRepo.watch(alice, label: :alice)} |
| 459 | + end, prefix: tenant) |
| 460 | + assigns = [alice: alice] |
| 461 | + {:ok, %{assigns: assigns, futures: [future]}} |
| 462 | + end |
| 463 | + |
| 464 | + # ... |
| 465 | + |
| 466 | + def handle_info({ref, :ready}, state=%{assigns: assigns, futures: futures}) when is_reference(ref) do |
| 467 | + {new_assigns, new_futures} = MyRepo.assign_ready(futures, [ref], watch?: true) |
| 468 | + {:noreply, %{ |
| 469 | + state | |
| 470 | + assigns: Map.merge(assigns, Enum.into(new_assigns, %{})), |
| 471 | + futures: new_futures |
| 472 | + }} |
| 473 | + end |
| 474 | + end |
| 475 | + ``` |
| 476 | + |
| 477 | + By handling the Future message resolution event `{ref, :ready}` in the `handle_info/2`, we never need to |
| 478 | + call `Repo.await/1`. Instead, our process can do other work and react to the `:ready` event immediately |
| 479 | + when it's delivered. |
| 480 | + |
| 481 | + And `Repo.assign_ready/3` provides a conventional way to retrieve the result and register a new watch |
| 482 | + in one go, so that updates to Alice are not missed. |
| 343 483 | |
| 344 484 | ## Upserts |
| 345 485 | |
| @@ -348,7 +488,7 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 348 488 | ### Upserts with `on_conflict` |
| 349 489 | |
| 350 490 | The following choices for `on_conflict` are supported, and they work in the manner |
| 351 | - described by the offical Ecto documentation. |
| 491 | + described by the official Ecto documentation. |
| 352 492 | |
| 353 493 | - `:raise`: The default. |
| 354 494 | - `:nothing` |
| @@ -376,7 +516,11 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 376 516 | for multiple network round trips. EctoFDB uses a `async`/`await` syntax to |
| 377 517 | implement pipelining. |
| 378 518 | |
| 379 | - All the "queryable" repo functions have `async` versions: |
| 519 | + ### EctoFDB Repo functions for pipelining |
| 520 | + |
| 521 | + EctoFDB adds the following functions to your Repo module. You'll notice that there are `async_*` |
| 522 | + versions of all the Queryable functions on the standard `Ecto.Repo`. These have the same arguments |
| 523 | + as their counterparts, and they each return a single `EctoFoundationDB.Future`. |
| 380 524 | |
| 381 525 | - `Repo.async_get/2` / `Repo.async_get/3` |
| 382 526 | - `Repo.async_get!/2` / `Repo.async_get!/3` |
| @@ -385,22 +529,24 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 385 529 | - `Repo.async_one/1` / `Repo.async_one/2` |
| 386 530 | - `Repo.async_one!/1` / `Repo.async_one!/2` |
| 387 531 | - `Repo.async_all/1` / `Repo.async_all/2` |
| 532 | + - `Repo.await/1`: The results of the above functions are provided to `Repo.await/1` to |
| 533 | + resolve the Futures. It accepts a single Future or a list of Futures. |
| 388 534 | |
| 389 | - The results of these functions are provided to `Repo.await/1` inside a `Repo.transaction/2`. |
| 535 | + ### Example |
| 390 536 | |
| 391 537 | Consider this example. Our transaction below has 2 network round trips in serial. |
| 392 | - The "John" User is retrieved, and then the "James" users is retrieved. Finally, the |
| 538 | + The "Alice" User is retrieved, and then the "Bob" user is retrieved. Finally, the |
| 393 539 | list is constructed and the transaction ends. |
| 394 540 | |
| 395 541 | ```elixir |
| 396 542 | result = MyApp.Repo.transaction(fn -> |
| 397 543 | [ |
| 398 | - MyApp.Repo.get_by(User, name: "John"), |
| 399 | - MyApp.Repo.get_by(User, name: "James") |
| 544 | + MyApp.Repo.get_by(User, name: "Alice"), |
| 545 | + MyApp.Repo.get_by(User, name: "Bob") |
| 400 546 | ] |
| 401 547 | end, prefix: tenant) |
| 402 548 | |
| 403 | - [john, james] = result |
| 549 | + [alice, bob] = result |
| 404 550 | ``` |
| 405 551 | |
| 406 552 | With pipelining, we can issue 2 `async_get_by` queries without waiting for their result, |
| @@ -410,14 +556,14 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 410 556 | ```elixir |
| 411 557 | result = MyApp.Repo.transaction(fn -> |
| 412 558 | futures = [ |
| 413 | - MyApp.Repo.async_get_by(User, name: "John"), |
| 414 | - MyApp.Repo.async_get_by(User, name: "James") |
| 559 | + MyApp.Repo.async_get_by(User, name: "Alice"), |
| 560 | + MyApp.Repo.async_get_by(User, name: "Bob") |
| 415 561 | ] |
| 416 562 | |
| 417 563 | MyApp.Repo.await(futures) |
| 418 564 | end, prefix: tenant) |
| 419 565 | |
| 420 | - [john, james] = result |
| 566 | + [alice, bob] = result |
| 421 567 | ``` |
| 422 568 | |
| 423 569 | Conceptually, only 1 network round-trip is required, so this will be faster than |
| @@ -472,6 +618,30 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 472 618 | |
| 473 619 | This happens automatically when you include the list of futures to your `Repo.await/1`. |
| 474 620 | |
| 621 | + ## Standard Options |
| 622 | + |
| 623 | + * `:cluster_file` - The path to the fdb.cluster file. The default is |
| 624 | + `"/etc/foundationdb/fdb.cluster"`. |
| 625 | + * `:migrator` - A module that implements the `EctoFoundationDB.Migrator` |
| 626 | + behaviour. Required when using any indexes. Defaults to `nil`. When `nil`, |
| 627 | + your Repo module is assumed to be the Migrator. |
| 628 | + |
| 629 | + ## Advanced Options |
| 630 | + |
| 631 | + * `:storage_id` - All tenants created by this adapter are prefixed with |
| 632 | + this string. This allows multiple configurations to operate on the |
| 633 | + same FoundationDB cluster independently. Defaults to |
| 634 | + `"Ecto.Adapters.FoundationDB"`. |
| 635 | + * `:open_db` - 1-arity function accepting the Repo module. Used for opening a reference to the |
| 636 | + FoundationDB cluster. Defaults to `&EctoFoundationDB.Database.open/1`. When |
| 637 | + using `EctoFoundationDB.Sandbox`, you should set this option to `Sandbox.open_db/1`. |
| 638 | + * `:migration_step` - The maximum number of keys to process in a single transaction |
| 639 | + when running migrations. Defaults to `1000`. If you use a number that is |
| 640 | + too large, the FDB transactions run by the Migrator will fail. |
| 641 | + * `:idx_cache` - When set to `:enabled`, the Ecto ets cache is used to store the |
| 642 | + available indexes per tenant. This speeds up all database operations. |
| 643 | + Defaults to `:enabled`. |
| 644 | + |
| 475 645 | ## Optimizing for throughput and latency |
| 476 646 | |
| 477 647 | Pipelining operations inside your transactions can offer significant speed-ups, and you should |
| @@ -487,30 +657,7 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 487 657 | If you're attempting to get the maximum throughput on data loading, you'll probably want to |
| 488 658 | make use of both! |
| 489 659 | |
| 490 | - ## Standard Options |
| 491 | - |
| 492 | - * `:cluster_file` - The path to the fdb.cluster file. The default is |
| 493 | - `"/etc/foundationdb/fdb.cluster"`. |
| 494 | - * `:migrator` - A module that implements the `EctoFoundationDB.Migrator` |
| 495 | - behaviour. Required when using any indexes. Defaults to `nil`. When `nil`, |
| 496 | - your Repo module is assumed to be the Migrator. |
| 497 | - |
| 498 | - ## Advanced Options |
| 499 | - |
| 500 | - * `:storage_id` - All tenants created by this adapter are prefixed with |
| 501 | - this string. This allows multiple configurations to operate on the |
| 502 | - same FoundationDB cluster indepedently. Defaults to |
| 503 | - `"Ecto.Adapters.FoundationDB"`. |
| 504 | - * `:open_db` - 0-arity function used for opening a reference to the |
| 505 | - FoundationDB cluster. Defaults to `:erlfdb.open(cluster_file)`. When |
| 506 | - using `EctoFoundationDB.Sandbox`, you should consider setting |
| 507 | - this option to `Sandbox.open_db/0`. |
| 508 | - * `:migration_step` - The maximum number of keys to process in a single transaction |
| 509 | - when running migrations. Defaults to `1000`. If you use a number that is |
| 510 | - too large, the FDB transactions run by the Migrator will fail. |
| 511 | - * `:idx_cache` - When set to `:enabled`, the Ecto ets cache is used to store the |
| 512 | - available indexes per tenant. This speeds up all database operations. |
| 513 | - Defaults to `:enabled`. |
| 660 | + Read more at [FDB Developer Guide | Loading Data](https://apple.github.io/foundationdb/developer-guide.html#loading-data). |
| 514 661 | |
| 515 662 | ## Limitations and Caveats |
| 516 663 | |
| @@ -581,13 +728,9 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 581 728 | 1. It makes sense for EctoFoundationDB, but it just hasn't been implemented yet. |
| 582 729 | 2. The fundamental differences between FDB and SQL backends mean that there is |
| 583 730 | no practical reason to implement it in EctoFoundationDB. |
| 584 | - |
| 585 | - Please feel free to request features, but also please be aware that your request |
| 586 | - might be closed if it falls into that second category. |
| 587 731 | """ |
| 588 732 | |
| 589 733 | @behaviour Ecto.Adapter |
| 590 | - @behaviour Ecto.Adapter.Storage |
| 591 734 | @behaviour Ecto.Adapter.Schema |
| 592 735 | @behaviour Ecto.Adapter.Queryable |
| 593 736 | @behaviour Ecto.Adapter.Transaction |
| @@ -599,22 +742,20 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 599 742 | alias EctoFoundationDB.Tenant |
| 600 743 | |
| 601 744 | alias Ecto.Adapters.FoundationDB.EctoAdapter |
| 745 | + alias Ecto.Adapters.FoundationDB.EctoAdapterAssigns |
| 746 | + alias Ecto.Adapters.FoundationDB.EctoAdapterAsync |
| 602 747 | alias Ecto.Adapters.FoundationDB.EctoAdapterQueryable |
| 603 748 | alias Ecto.Adapters.FoundationDB.EctoAdapterSchema |
| 604 | - alias Ecto.Adapters.FoundationDB.EctoAdapterStorage |
| 605 749 | alias Ecto.Adapters.FoundationDB.EctoAdapterTransaction |
| 606 750 | |
| 607 751 | @spec db(Ecto.Repo.t()) :: Database.t() |
| 608 752 | def db(repo) when is_atom(repo) do |
| 609 | - db(repo.config()) |
| 610 | - end |
| 611 | - |
| 612 | - @spec db(Options.t()) :: Database.t() |
| 613 | - def db(options) do |
| 614 | - case :persistent_term.get({__MODULE__, :database}, nil) do |
| 753 | + case :persistent_term.get({__MODULE__, repo, :database}, nil) do |
| 615 754 | nil -> |
| 616 | - db = EctoAdapterStorage.open_db(options) |
| 617 | - :persistent_term.put({__MODULE__, :database}, {db, options}) |
| 755 | + options = repo.config() |
| 756 | + fun = Options.get(options, :open_db) |
| 757 | + db = fun.(repo) |
| 758 | + :persistent_term.put({__MODULE__, repo, :database}, {db, options}) |
| 618 759 | db |
| 619 760 | |
| 620 761 | {db, _options} -> |
| @@ -641,8 +782,8 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 641 782 | For example, a transaction must complete |
| 642 783 | [within 5 seconds](https://apple.github.io/foundationdb/developer-guide.html#long-running-transactions). |
| 643 784 | """ |
| 644 | - @spec transactional(Database.t() | Tenant.t() | nil, function()) :: any() |
| 645 | - def transactional(db_or_tenant, fun), do: Tx.transactional_external(db_or_tenant, fun) |
| 785 | + @spec transactional(Tenant.t() | nil, function()) :: any() |
| 786 | + def transactional(tenant, fun), do: Tx.transactional_external(tenant, fun) |
| 646 787 | |
| 647 788 | @impl Ecto.Adapter |
| 648 789 | defmacro __before_compile__(_env) do |
| @@ -671,37 +812,57 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 671 812 | do: async_query(fn -> all(queryable, opts ++ [returning: {:future, :all}]) end) |
| 672 813 | |
| 673 814 | defp async_query(fun) do |
| 674 | - _res = fun.() |
| 675 | - |
| 676 | - case Process.delete(Future.token()) do |
| 677 | - nil -> |
| 678 | - raise "Pipelining failure" |
| 679 | - |
| 680 | - future -> |
| 681 | - future |
| 682 | - end |
| 683 | - after |
| 684 | - Process.delete(Future.token()) |
| 815 | + repo = get_dynamic_repo() |
| 816 | + EctoAdapterAsync.async_query(__MODULE__, repo, fun) |
| 685 817 | end |
| 686 818 | |
| 687 | - def await(futures) do |
| 819 | + def await(futures) when is_list(futures) do |
| 688 820 | futures |
| 689 821 | |> Future.await_all() |
| 690 | - |> Enum.map(fn future -> |
| 691 | - result = Future.result(future) |
| 692 | - if is_nil(result), do: raise("Pipelining failure") |
| 822 | + |> Enum.map(&Future.result/1) |
| 823 | + end |
| 693 824 | |
| 694 | - # Abuse a :noop option here to signal to the backend that we don't |
| 695 | - # actually want to run a query. Instead, we just want the result to |
| 696 | - # be transformed by Ecto's internal logic. |
| 697 | - case Future.all_or_one(future) do |
| 698 | - :all -> |
| 699 | - all(Future.schema(future), noop: result) |
| 825 | + def await(future) do |
| 826 | + [res] = await([future]) |
| 827 | + res |
| 828 | + end |
| 700 829 | |
| 701 | - :one -> |
| 702 | - one(Future.schema(future), noop: result) |
| 703 | - end |
| 704 | - end) |
| 830 | + def watch(struct, options \\ []) do |
| 831 | + repo = get_dynamic_repo() |
| 832 | + |
| 833 | + EctoAdapterSchema.watch( |
| 834 | + __MODULE__, |
| 835 | + repo, |
| 836 | + struct, |
| 837 | + Ecto.Repo.Supervisor.tuplet( |
| 838 | + repo, |
| 839 | + Keyword.merge(__MODULE__.default_options(:watch), options) |
| 840 | + ) |
| 841 | + ) |
| 842 | + end |
| 843 | + |
| 844 | + def assign_ready(futures, ready_refs, options \\ []) do |
| 845 | + repo = get_dynamic_repo() |
| 846 | + |
| 847 | + EctoAdapterAssigns.assign_ready( |
| 848 | + __MODULE__, |
| 849 | + repo, |
| 850 | + futures, |
| 851 | + ready_refs, |
| 852 | + options |
| 853 | + ) |
| 854 | + end |
| 855 | + |
| 856 | + def async_assign_ready(futures, ready_ref, options \\ []) do |
| 857 | + repo = get_dynamic_repo() |
| 858 | + |
| 859 | + EctoAdapterAssigns.async_assign_ready( |
| 860 | + __MODULE__, |
| 861 | + repo, |
| 862 | + futures, |
| 863 | + ready_ref, |
| 864 | + options |
| 865 | + ) |
| 705 866 | end |
| 706 867 | end |
| 707 868 | end |
| @@ -724,15 +885,6 @@ defmodule Ecto.Adapters.FoundationDB do | |
| 724 885 | @impl Ecto.Adapter |
| 725 886 | defdelegate dumpers(primitive_type, ecto_type), to: EctoAdapter |
| 726 887 | |
| 727 | - @impl Ecto.Adapter.Storage |
| 728 | - defdelegate storage_up(options), to: EctoAdapterStorage |
| 729 | - |
| 730 | - @impl Ecto.Adapter.Storage |
| 731 | - defdelegate storage_down(options), to: EctoAdapterStorage |
| 732 | - |
| 733 | - @impl Ecto.Adapter.Storage |
| 734 | - defdelegate storage_status(options), to: EctoAdapterStorage |
| 735 | - |
| 736 888 | @impl Ecto.Adapter.Schema |
| 737 889 | defdelegate autogenerate(type), to: EctoAdapterSchema |
| @@ -16,7 +16,7 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapter do | |
| 16 16 | telemetry_prefix = Keyword.fetch!(config, :telemetry_prefix) |
| 17 17 | telemetry = {config[:repo], log, telemetry_prefix ++ [:query]} |
| 18 18 | |
| 19 | - {:ok, Supervisor.child_spec(Ecto.Adapters.FoundationDB.Supervisor, []), |
| 19 | + {:ok, Ecto.Adapters.FoundationDB.Supervisor.child_spec([]), |
| 20 20 | %{telemetry: telemetry, stacktrace: stacktrace, opts: config}} |
| 21 21 | end |
Loading more files…