Current section

20 Versions

Jump to

Compare versions

26 files changed
+1786 additions
-145 deletions
  @@ -1,5 +1,21 @@
1 1 # Changelog
2 2
3 + ## v0.6.0 (2025-12-27)
4 +
5 + ### Enhancements
6 +
7 + * SchemaMetadata now supports indexed fields
8 + * `EctoFoundationDB.Sync`: Defines conventions for keeping a stateful process (e.g. LiveView) automatically up-to-date
9 + with the latest data from the database.
10 +
11 + ### Dependencies
12 +
13 + * `ecto ~> 3.13`
14 +
15 + ### New documentation
16 +
17 + * [Sync Engine Part III - Batteries Included](sync_module.livemd): New Livebook that demonstrates how to use the `EctoFoundationDB.Sync` module in LiveView.
18 +
3 19 ## v0.5.1 (2025-12-19)
4 20
5 21 ### Enhancements
  @@ -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, "~> 0.4"}
28 + {:ecto_foundationdb, "~> 0.6"}
29 29 ]
30 30 end
31 31 ```
  @@ -2,7 +2,7 @@
2 2 [{<<"GitHub">>,
3 3 <<"https://github.com/foundationdb-beam/ecto_foundationdb">>}]}.
4 4 {<<"name">>,<<"ecto_foundationdb">>}.
5 - {<<"version">>,<<"0.5.1">>}.
5 + {<<"version">>,<<"0.6.0">>}.
6 6 {<<"description">>,<<"FoundationDB adapter for Ecto">>}.
7 7 {<<"elixir">>,<<"~> 1.15">>}.
8 8 {<<"app">>,<<"ecto_foundationdb">>}.
  @@ -16,12 +16,22 @@
16 16 [{<<"name">>,<<"ecto">>},
17 17 {<<"app">>,<<"ecto">>},
18 18 {<<"optional">>,false},
19 - {<<"requirement">>,<<"~> 3.12">>},
19 + {<<"requirement">>,<<"~> 3.13">>},
20 20 {<<"repository">>,<<"hexpm">>}],
21 21 [{<<"name">>,<<"jason">>},
22 22 {<<"app">>,<<"jason">>},
23 23 {<<"optional">>,false},
24 24 {<<"requirement">>,<<"~> 1.4">>},
25 + {<<"repository">>,<<"hexpm">>}],
26 + [{<<"name">>,<<"phoenix">>},
27 + {<<"app">>,<<"phoenix">>},
28 + {<<"optional">>,true},
29 + {<<"requirement">>,<<"~> 1.0">>},
30 + {<<"repository">>,<<"hexpm">>}],
31 + [{<<"name">>,<<"phoenix_live_view">>},
32 + {<<"app">>,<<"phoenix_live_view">>},
33 + {<<"optional">>,true},
34 + {<<"requirement">>,<<"~> 1.1">>},
25 35 {<<"repository">>,<<"hexpm">>}]]}.
26 36 {<<"files">>,
27 37 [<<"lib">>,<<"lib/ecto_foundationdb">>,
  @@ -31,7 +41,7 @@
31 41 <<"lib/ecto_foundationdb/tenant/directory_tenant.ex">>,
32 42 <<"lib/ecto_foundationdb/tenant/managed_tenant.ex">>,
33 43 <<"lib/ecto_foundationdb/database.ex">>,
34 - <<"lib/ecto_foundationdb/options.ex">>,
44 + <<"lib/ecto_foundationdb/options.ex">>,<<"lib/ecto_foundationdb/sync.ex">>,
35 45 <<"lib/ecto_foundationdb/tenant.ex">>,
36 46 <<"lib/ecto_foundationdb/migrations_pj.ex">>,
37 47 <<"lib/ecto_foundationdb/cli">>,<<"lib/ecto_foundationdb/cli/internal.ex">>,
  @@ -58,11 +68,18 @@
58 68 <<"lib/ecto_foundationdb/schema.ex">>,
59 69 <<"lib/ecto_foundationdb/schema_migration.ex">>,
60 70 <<"lib/ecto_foundationdb/sandbox.ex">>,<<"lib/ecto_foundationdb/cli.ex">>,
71 + <<"lib/ecto_foundationdb/sync">>,<<"lib/ecto_foundationdb/sync/all.ex">>,
72 + <<"lib/ecto_foundationdb/sync/one.ex">>,
73 + <<"lib/ecto_foundationdb/sync/idlist.ex">>,
74 + <<"lib/ecto_foundationdb/sync/state.ex">>,
75 + <<"lib/ecto_foundationdb/sync/many.ex">>,
76 + <<"lib/ecto_foundationdb/sync/lifecycle.ex">>,
61 77 <<"lib/ecto_foundationdb/versionstamp.ex">>,
62 78 <<"lib/ecto_foundationdb/indexer.ex">>,
63 79 <<"lib/ecto_foundationdb/exception">>,
64 80 <<"lib/ecto_foundationdb/exception/unsupported.ex">>,
65 81 <<"lib/ecto_foundationdb/exception/incorrect_tenancy.ex">>,
82 + <<"lib/ecto_foundationdb/watch_janitor.ex">>,
66 83 <<"lib/ecto_foundationdb/indexer">>,
67 84 <<"lib/ecto_foundationdb/indexer/schema_metadata.ex">>,
68 85 <<"lib/ecto_foundationdb/indexer/mdv_app_version.ex">>,
  @@ -26,7 +26,7 @@ defmodule Ecto.Adapters.FoundationDB do
26 26 ```elixir
27 27 defp deps do
28 28 [
29 - {:ecto_foundationdb, "~> 0.5"}
29 + {:ecto_foundationdb, "~> 0.6"}
30 30 ]
31 31 end
32 32 ```
  @@ -636,11 +636,11 @@ defmodule Ecto.Adapters.FoundationDB do
636 636 # ...
637 637
638 638 def handle_info({ref, :ready}, state=%{assigns: assigns, futures: futures, tenant: tenant}) when is_reference(ref) do
639 - {new_assigns, new_futures} = MyRepo.assign_ready(futures, [ref], watch?: true, prefix: tenant)
639 + {new_assigns, new_futures, other_futures} = MyRepo.assign_ready(futures, [ref], watch?: true, prefix: tenant)
640 640 {:noreply, %{
641 641 state |
642 642 assigns: Map.merge(assigns, Enum.into(new_assigns, %{})),
643 - futures: new_futures
643 + futures: other_futures ++ new_futures
644 644 }}
645 645 end
646 646 end
  @@ -699,6 +699,15 @@ defmodule Ecto.Adapters.FoundationDB do
699 699 end
700 700 ```
701 701
702 + You may also track metadata by indexed fields. The following metadata creation will track the counters listed above for each value of `name`.
703 + In other words, there will be a set of counters specific to all operations on `User` where `name: "Alice"`, and so on.
704 +
705 + ```elixir
706 + def change() do
707 + [create metadata(User, [:name])]
708 + end
709 + ```
710 +
702 711 For more on these watches, see [Sync Engine Part II - Collections](collection_syncing.html)
703 712
704 713 ## Upserts
  @@ -1025,7 +1034,7 @@ defmodule Ecto.Adapters.FoundationDB do
1025 1034 end
1026 1035
1027 1036 def async_all_range(queryable, id_s, id_e, opts \\ []) do
1028 - async_query(fn ->
1037 + async_query(queryable, fn ->
1029 1038 repo = get_dynamic_repo()
1030 1039
1031 1040 EctoAdapterQueryable.execute_all_range(
  @@ -1051,31 +1060,48 @@ defmodule Ecto.Adapters.FoundationDB do
1051 1060 end
1052 1061
1053 1062 def async_get(queryable, id, opts \\ []),
1054 - do: async_query(fn -> get(queryable, id, opts ++ [returning: {:future, :one}]) end)
1063 + do:
1064 + async_query(queryable, fn ->
1065 + get(queryable, id, opts ++ [returning: {:future, :one}])
1066 + end)
1055 1067
1056 1068 def async_get!(queryable, id, opts \\ []),
1057 - do: async_query(fn -> get!(queryable, id, opts ++ [returning: {:future, :one}]) end)
1069 + do:
1070 + async_query(queryable, fn ->
1071 + get!(queryable, id, opts ++ [returning: {:future, :one}])
1072 + end)
1058 1073
1059 1074 def async_get_by(queryable, clauses, opts \\ []),
1060 1075 do:
1061 - async_query(fn -> get_by(queryable, clauses, opts ++ [returning: {:future, :one}]) end)
1076 + async_query(queryable, fn ->
1077 + get_by(queryable, clauses, opts ++ [returning: {:future, :one}])
1078 + end)
1062 1079
1063 1080 def async_get_by!(queryable, clauses, opts \\ []),
1064 1081 do:
1065 - async_query(fn -> get_by!(queryable, clauses, opts ++ [returning: {:future, :one}]) end)
1082 + async_query(queryable, fn ->
1083 + get_by!(queryable, clauses, opts ++ [returning: {:future, :one}])
1084 + end)
1066 1085
1067 1086 def async_one(queryable, opts \\ []),
1068 - do: async_query(fn -> one(queryable, opts ++ [returning: {:future, :one}]) end)
1087 + do: async_query(queryable, fn -> one(queryable, opts ++ [returning: {:future, :one}]) end)
1069 1088
1070 1089 def async_one!(queryable, opts \\ []),
1071 - do: async_query(fn -> one!(queryable, opts ++ [returning: {:future, :one}]) end)
1090 + do:
1091 + async_query(queryable, fn -> one!(queryable, opts ++ [returning: {:future, :one}]) end)
1072 1092
1073 1093 def async_all(queryable, opts \\ []),
1074 - do: async_query(fn -> all(queryable, opts ++ [returning: {:future, :all}]) end)
1094 + do: async_query(queryable, fn -> all(queryable, opts ++ [returning: {:future, :all}]) end)
1075 1095
1076 - defp async_query(fun) do
1096 + def async_all_by(queryable, by, opts \\ []),
1097 + do:
1098 + async_query(queryable, fn ->
1099 + all_by(queryable, by, opts ++ [returning: {:future, :all}])
1100 + end)
1101 +
1102 + defp async_query(queryable, fun) do
1077 1103 repo = get_dynamic_repo()
1078 - EctoAdapterAsync.async_query(__MODULE__, repo, fun)
1104 + EctoAdapterAsync.async_query(__MODULE__, repo, queryable, fun)
1079 1105 end
1080 1106
1081 1107 def await(futures) when is_list(futures) do
  @@ -7,66 +7,98 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterAssigns do
7 7 alias Ecto.Adapters.FoundationDB
8 8
9 9 def assign_ready(_module, repo, futures, ready_refs, options) when is_list(ready_refs) do
10 - Tx.transactional(options[:prefix], fn _tx ->
11 - {assign_futures_rev, futures} = filter_ready(repo, futures, ready_refs, options)
10 + {labeled_res, untouched_futures} =
11 + Tx.transactional(options[:prefix], fn _tx ->
12 + {labeled_futures_rev, futures} = filter_ready(repo, futures, ready_refs, options)
12 13
13 - res = repo.await(Enum.reverse(assign_futures_rev))
14 -
15 - Enum.reduce(res, {[], futures}, fn
16 - {new_assigns, new_future_or_nil}, {assigns, futures} ->
17 - {assigns ++ new_assigns, append_new_future(futures, new_future_or_nil)}
14 + labeled_futures = Enum.reverse(labeled_futures_rev)
15 + {labels, assign_futures} = Enum.unzip(labeled_futures)
16 + res = repo.await(assign_futures)
17 + labeled_res = Enum.zip(labels, res)
18 + {labeled_res, futures}
18 19 end)
19 - end)
20 +
21 + empty_futures = if is_list(futures), do: [], else: %{}
22 +
23 + {assigns, new_futures} =
24 + Enum.reduce(labeled_res, {[], empty_futures}, fn
25 + {label, {new_assigns, new_future_or_nil}}, {assigns, new_futures} ->
26 + {assigns ++ new_assigns, append_new_future(new_futures, label, new_future_or_nil)}
27 + end)
28 +
29 + {assigns, new_futures, untouched_futures}
20 30 end
21 31
22 32 defp filter_ready(repo, futures, ready_refs, options) do
23 33 Enum.reduce(ready_refs, {[], futures}, fn ready_ref, {acc, futures} ->
24 34 case async_assign_ready(__MODULE__, repo, futures, ready_ref, options) do
25 - {nil, futures} ->
35 + {_label, nil, futures} ->
26 36 {acc, futures}
27 37
28 - {assign_future, futures} ->
29 - {[assign_future | acc], futures}
38 + {label, assign_future, futures} ->
39 + {[{label, assign_future} | acc], futures}
30 40 end
31 41 end)
32 42 end
33 43
34 - defp append_new_future(futures, nil), do: futures
35 - defp append_new_future(futures, future), do: [future | futures]
44 + defp append_new_future(futures, _label, nil), do: futures
45 + defp append_new_future(futures, _label, future) when is_list(futures), do: [future | futures]
46 +
47 + defp append_new_future(futures, label, future) when is_map(futures) do
48 + case Map.fetch(futures, label) do
49 + {:ok, fl} when is_list(fl) ->
50 + Map.put(futures, label, [future | fl])
51 +
52 + {:ok, f} ->
53 + Future.cancel(f)
54 + Map.put(futures, label, future)
55 +
56 + _ ->
57 + Map.put(futures, label, future)
58 + end
59 + end
36 60
37 61 def async_assign_ready(_module, repo, futures, ready_ref, options)
38 62 when is_reference(ready_ref) do
39 63 case Future.find_ready(futures, ready_ref) do
40 - {nil, futures} ->
41 - {nil, futures}
64 + {label, nil, futures} ->
65 + {label, nil, futures}
42 66
43 - {future, futures} ->
44 - {schema, kind, watch_options, new_watch_fn} = Future.result(future)
67 + {label, found_future, futures} ->
68 + {schema, kind, watch_options, new_watch_fn} = Future.result(found_future)
45 69
46 - if not Keyword.has_key?(watch_options, :label) do
70 + if is_nil(label) and not Keyword.has_key?(watch_options, :label) do
47 71 raise """
48 - To use Repo.assign_ready/3, you must have previously created a watch with a label
72 + To use Repo.assign_ready/3, you must either
73 +
74 + 1. Create the original watch with a label
49 75
50 76 Examples:
51 77
52 78 Repo.watch(struct, label: :mykey)
53 79 SchemaMetadata.watch_collection(MySchema, label: :mykey)
80 +
81 + 2. Call Repo.assign_ready/3 with a labeled map of futures
82 +
83 + Example:
84 +
85 + Repo.assign_ready(%{mykey: future}, [ready_ref], watch?: true, prefix: tenant)
54 86 """
55 87 end
56 88
57 89 case kind do
58 90 {:pk, pk} ->
59 - async_get(repo, futures, schema, pk, watch_options, options, new_watch_fn)
91 + async_get(repo, label, futures, schema, pk, watch_options, options, new_watch_fn)
60 92
61 - {SchemaMetadata, name}
62 - when name in [:inserts, :deletes, :collection, :updates, :changes] ->
63 - async_all(repo, futures, schema, watch_options, options, new_watch_fn)
93 + {SchemaMetadata, by, action}
94 + when action in [:inserts, :deletes, :collection, :updates, :changes] ->
95 + async_all_by(repo, label, by, futures, schema, watch_options, options, new_watch_fn)
64 96 end
65 97 end
66 98 end
67 99
68 - defp async_get(repo, futures, schema, id, watch_options, options, new_watch_fn) do
69 - label = watch_options[:label]
100 + defp async_get(repo, label, futures, schema, id, watch_options, options, new_watch_fn) do
101 + label = label || watch_options[:label]
70 102
71 103 tenant = options[:prefix]
72 104
  @@ -80,32 +112,43 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterAssigns do
80 112 {[{label, struct_or_nil}], new_future}
81 113 end)
82 114
83 - {assign_future, futures}
115 + {label, assign_future, futures}
84 116 end)
85 117 end
86 118
87 - defp async_all(repo, futures, schema, watch_options, options, new_watch_fn) do
88 - label = watch_options[:label]
119 + defp async_all_by(repo, label, by, futures, schema, watch_options, options, new_watch_fn) do
120 + label = label || watch_options[:label]
89 121 query = watch_options[:query] || schema
90 122 tenant = options[:prefix]
91 123
92 124 Tx.transactional(tenant, fn _tx ->
93 125 assign_future =
94 - repo.async_all(query, options)
95 - |> Future.apply(fn result ->
126 + case by do
127 + [] ->
128 + repo.async_all(query, options)
129 +
130 + _ ->
131 + repo.async_all_by(query, by, options)
132 + end
133 +
134 + assign_future =
135 + Future.apply(assign_future, fn result ->
96 136 result = usetenant(result, tenant)
97 137 new_future = maybe_new_watch(result, watch_options, options, new_watch_fn)
98 138
99 139 {[{label, result}], new_future}
100 140 end)
101 141
102 - {assign_future, futures}
142 + {label, assign_future, futures}
103 143 end)
104 144 end
105 145
106 - defp usetenant(nil, _tenant), do: nil
107 146 defp usetenant(list, tenant) when is_list(list), do: Enum.map(list, &usetenant(&1, tenant))
108 - defp usetenant(struct, tenant), do: FoundationDB.usetenant(struct, tenant)
147 +
148 + defp usetenant(struct, tenant) when is_struct(struct),
149 + do: FoundationDB.usetenant(struct, tenant)
150 +
151 + defp usetenant(data, _tenant), do: data
109 152
110 153 defp maybe_new_watch(result, watch_options, options, new_watch_fn) do
111 154 if Keyword.get(options, :watch?, false) do
Loading more files…