Skip to content

Commit

Permalink
Multiple index fields
Browse files Browse the repository at this point in the history
  • Loading branch information
JesseStimpson committed Mar 30, 2024
1 parent 4d737e1 commit 132825c
Show file tree
Hide file tree
Showing 25 changed files with 770 additions and 343 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Roughly in order of priority.
- [x] Benchmarking
- [x] Index caching
- [x] Mapped Range Indexes
- [ ] Hierarchical multi index
- [x] Hierarchical multi index
- [ ] FDB Watches
- [ ] Ecto Transactions
- [ ] Logging for all Adapter behaviours
Expand Down
5 changes: 3 additions & 2 deletions bench/support/migrations.exs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
defmodule Ecto.Bench.CreateUser do
alias Ecto.Bench.User
use Ecto.Adapters.FoundationDB.Migration

def change do
[
create(index(:users, [:name])),
create(index(:users, [:uuid]))
create(index(User, [:name])),
create(index(User, [:uuid]))
]
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/ecto/adapters/foundationdb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ defmodule Ecto.Adapters.FoundationDB do
use Ecto.Adapters.FoundationDB.Migration
def change() do
[
create(index(:users, [:name]),
create(index(:posts, [:user_id]))
create(index(User, [:name]),
create(index(Post, [:user_id]))
]
end
end
Expand Down
13 changes: 4 additions & 9 deletions lib/ecto/adapters/foundationdb/ecto_adapter_migration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterMigration do
alias Ecto.Adapters.FoundationDB.Exception.Unsupported
alias Ecto.Adapters.FoundationDB.Layer.IndexInventory
alias Ecto.Adapters.FoundationDB.Migration.Index
alias Ecto.Adapters.FoundationDB.Schema

@impl true
def supports_ddl_transaction?() do
Expand All @@ -22,21 +23,15 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterMigration do
{:create,
%Index{
prefix: tenant,
table: source,
schema: schema,
name: index_name,
columns: index_fields,
options: options
}},
_options
) do
:ok =
IndexInventory.create_index(
tenant,
source,
index_name,
index_fields,
options
)
source = Schema.get_source(schema)
:ok = IndexInventory.create_index(tenant, schema, source, index_name, index_fields, options)

{:ok, []}
end
Expand Down
15 changes: 4 additions & 11 deletions lib/ecto/adapters/foundationdb/ecto_adapter_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,14 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterSchema do

num_ins =
IndexInventory.transactional(tenant, adapter_meta, source, fn tx, idxs ->
Tx.insert_all(tx, source, context, entries, idxs)
Tx.insert_all(tx, schema, source, context, entries, idxs)
end)

{num_ins, nil}
end

@impl Ecto.Adapter.Schema
def insert(
adapter_meta,
schema_meta,
data_object,
on_conflict,
returning,
options
) do
def insert(adapter_meta, schema_meta, data_object, on_conflict, returning, options) do
{1, nil} =
insert_all(
adapter_meta,
Expand Down Expand Up @@ -87,7 +80,7 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterSchema do

res =
IndexInventory.transactional(tenant, adapter_meta, source, fn tx, idxs ->
Tx.update_pks(tx, source, context, pk_field, [pk], update_data, idxs)
Tx.update_pks(tx, schema, source, context, pk_field, [pk], update_data, idxs)
end)

case res do
Expand Down Expand Up @@ -115,7 +108,7 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterSchema do

res =
IndexInventory.transactional(tenant, adapter_meta, source, fn tx, idxs ->
Tx.delete_pks(tx, source, [pk], idxs)
Tx.delete_pks(tx, schema, source, [pk], idxs)
end)

case res do
Expand Down
82 changes: 51 additions & 31 deletions lib/ecto/adapters/foundationdb/layer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer do
[%User{id: "some-uuid", name: "John", department: "Engineering"}]
Note: The Primary Write can be skipped by providing the `write_primary: false` option on the `@schema_context`.
See Time Series Index for more.
See below for more.
## Index Write and Read
Expand All @@ -53,7 +53,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer do
defmodule MyApp.Migration do
use Ecto.Adapters.FoundationDB.Migration
def change() do
[create(index(:users, [:department]))]
[create(index(User, [:department]))]
end
end
```
Expand All @@ -64,49 +64,69 @@ defmodule Ecto.Adapters.FoundationDB.Layer do
iex> query = from(u in User, where: u.department == ^"Engineering")
iex> Repo.all(query, prefix: tenant)
The index value can be any Elixir term.
The index value can be any Elixir term. All types support Equal queries. However, certain Ecto Schema types
support Between queries. For example, you can define an index on a naive_datetime_usec field to construct
an effective time series index.
Suggestion: Before you create an index, we suggest you test your workload without the index first. You may
be surprised by the efficiency in which `Repo.all(User, prefix: tenant)` can return data. Once you have
the data, you can do sophisticated filtering within Elixir itself.
iex> query = from(e in Event,
...> where: e.timestamp >= ^~N[2024-01-01 00:00:00] and e.timestamp < ^~N[2024-01-01 12:00:00]
...> )
iex> Repo.all(query, prefix: tenant)
## Time Series Index
### Multiple index fields
This is a special kind of Default index that requires the indexed value to be `:naive_datetime_usec`. This
index allows a query to retrieve objects that have a datetime that exists in between two given endpoints
of a timespan.
**Order matters!**: When you create an index using multiple fields, the FDB key that stores the index will be extended with
all the values in the order of your defined index fields. For example, you may want to have a time series index, but also
have the ability to easily drop all events on a certain date. You can achieve this by creating an index on `[:date, :user_id, :time]`.
The order of the fields determines the Between queries you can perform.
```elixir
defmodule EctoFoundationDB.Schemas.Event do
use Ecto.Schema
@schema_context usetenant: true, write_primary: false
schema "events" do
field(:timestamp, :naive_datetime_usec)
field(:data, :string)
timestamps()
defmodule MyApp.Migration do
use Ecto.Adapters.FoundationDB.Migration
def change() do
[create(index(Event, [:date, :user_id, :time]))]
end
end
````
With this index, the following queries will be efficient:
```elixir
iex> query = from(e in Event,
...> where: e.date == ^~D[2024-01-01] and
e.user_id == ^"user-id" and
e.time >= ^~T[12:00:00] and e.time < ^~T[13:00:00]
...> )
iex> query = from(e in Event,
...> where: e.date >= ^~D[2024-01-01] and e.date < ^~D[2024-01-02]
...> )
```
However, this query will raise a Runtime exception:
```elixir
defmodule EctoFoundationDB.Migration do
use Ecto.Adapters.FoundationDB.Migration
def change() do
[create(index(:events, [:timestamp], options: [indexer: :timeseries]))]
end
end
iex> query = from(e in Event,
...> where: e.date >= ^~D[2024-01-01] and e.date < ^~D[2024-01-02]
e.user_id == ^"user-id"
...> )
```
Take note of the option `indexer: :timeseries` on the index creation in the Migration module.
There can be 0 or 1 Between clauses, and if one exists, it must correspond to the final constraint in the where clause when
compared against the order of the index fields.
Also notice that in the Schema, we choose to use `write_primary: false`. This skips the Primary Write.
However, this means that the data can **only** be managed by providing a timespan query. It also means
that indexes cannot be created in the future, because indexes are always initialized from the Primary Write.
### Advanced Options: `write_primary: false`, `mapped?: false`, and `from: <index_name>`
iex> query = from(e in Event,
...> where: e.timestamp >= ^~N[2024-01-01 00:00:00] and e.timestamp < ^~N[2024-01-01 12:00:00]
...> )
iex> Repo.all(query, prefix: tenant)
If you choose to use `write_primary: false` on your schema, this skips the Primary Write. The consequence of this are as follows:
1. You'll want to make sure your index is created with an option `mapped?: false`. This ensures that the
struct data is written to the index keyspace.
2. Your index queries will now have performance characteristics similar to a primary key query. That is, you'll
be able to retrieve the struct data with a single `erlfdb:get_range/3`.
3. The data can **only** be managed by providing a query on the index. You will not be able to access the data
via the primary key.
4. If a later migration creates a new index, it must provide the `from: <index_name>` option to specify the
index to read the data from.
## User-defined Indexes
Expand Down
Loading

0 comments on commit 132825c

Please sign in to comment.