Current section
20 Versions
Jump to
Current section
20 Versions
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…