Current section

20 Versions

Jump to

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…