Skip to content

Commit

Permalink
Refactor keyspace to use tuples instead of slash-paths
Browse files Browse the repository at this point in the history
  • Loading branch information
JesseStimpson committed Mar 24, 2024
1 parent a077b9c commit 841ed79
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 160 deletions.
2 changes: 0 additions & 2 deletions lib/ecto/adapters/foundationdb.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ defmodule Ecto.Adapters.FoundationDB do
## Advanced Options
* `:key_delimiter` - Keys in the database are constructed using this
delimiter in between segments. Defaults to "/".
* `:storage_id` - All tenants created by this adapter are prefixed with
this string. This allows multiple configurations to operate on the
same FoundationDB cluster indepedently. Defaults to
Expand Down
8 changes: 4 additions & 4 deletions lib/ecto/adapters/foundationdb/ecto_adapter_storage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterStorage do

@all_data_start_key ""
@all_data_end_key <<0xFF>>
@repo_data_start_key ""
@repo_data_end_key <<0xFEFF>>

def list_tenants(dbtx, options) do
start_key = get_tenant_name("", options)
Expand Down Expand Up @@ -69,7 +67,8 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterStorage do
tenant = open_tenant(dbtx, tenant_id, options)

:erlfdb.transactional(tenant, fn tx ->
:erlfdb.clear_range(tx, @repo_data_start_key, @repo_data_end_key)
{start_key, end_key} = Pack.adapter_repo_range()
:erlfdb.clear_range(tx, start_key, end_key)
end)

:ok
Expand Down Expand Up @@ -152,7 +151,8 @@ defmodule Ecto.Adapters.FoundationDB.EctoAdapterStorage do

defp get_tenant_name(tenant_id, options) do
storage_id = Options.get(options, :storage_id)
Pack.to_raw_fdb_key(options, <<>>, ["#{storage_id}", tenant_id])

"#{storage_id}/" <> tenant_id
end

defp get_tenant(dbtx, tenant_id, options) do
Expand Down
29 changes: 11 additions & 18 deletions lib/ecto/adapters/foundationdb/layer/index_inventory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer.IndexInventory do
index_fields,
options
) do
{inventory_key, idx} = new_index(adapter_meta, source, index_name, index_fields, options)
{inventory_key, idx} = new_index(source, index_name, index_fields, options)

Tx.transactional(db_or_tenant, fn tx ->
# Write a key that indicates the index exists. All other operations will
Expand All @@ -58,12 +58,13 @@ defmodule Ecto.Adapters.FoundationDB.Layer.IndexInventory do
## Examples
iex> Ecto.Adapters.FoundationDB.Layer.IndexInventory.new_index(%{opts: []}, "users", "users_name_index", [:name], [])
{"\\xFE\\xFFindexes/users/users_name_index", [id: "users_name_index", indexer: Ecto.Adapters.FoundationDB.Layer.Indexer.Default, source: "users", fields: [:name], options: []]}
iex> {key, obj} = Ecto.Adapters.FoundationDB.Layer.IndexInventory.new_index("users", "users_name_index", [:name], [])
iex> {:erlfdb_tuple.unpack(key), obj}
{{"\\xFE", "\\xFFindexes", "users", "users_name_index"}, [id: "users_name_index", indexer: Ecto.Adapters.FoundationDB.Layer.Indexer.Default, source: "users", fields: [:name], options: []]}
"""
def new_index(%{opts: adapter_opts}, source, index_name, index_fields, options) do
inventory_key = Pack.to_raw_fdb_key(adapter_opts, [source(), source, index_name])
def new_index(source, index_name, index_fields, options) do
inventory_key = Pack.namespaced_pack(source(), source, ["#{index_name}"])

idx = [
id: index_name,
Expand Down Expand Up @@ -158,7 +159,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer.IndexInventory do

defp tx_idxs_get(tx, adapter_opts, source, {vsn, idxs}) do
max_version_key =
MaxValue.key(adapter_opts, SchemaMigration.source(), @max_version_name)
MaxValue.key(SchemaMigration.source(), @max_version_name)

max_version_future = :erlfdb.get(tx, max_version_key)

Expand Down Expand Up @@ -189,10 +190,12 @@ defmodule Ecto.Adapters.FoundationDB.Layer.IndexInventory do
{vsn, idxs, vsn_validator}
end

defp tx_idxs_get_wait(tx, adapter_opts, source, max_version_future) do
defp tx_idxs_get_wait(tx, _adapter_opts, source, max_version_future) do
{start_key, end_key} = Pack.namespaced_range(source(), source, [])

idxs =
tx
|> :erlfdb.get_range_startswith(source_range_startswith(adapter_opts, source))
|> :erlfdb.get_range(start_key, end_key)
|> :erlfdb.wait()
|> Enum.map(fn {_, fdb_value} -> Pack.from_fdb_value(fdb_value) end)

Expand All @@ -204,16 +207,6 @@ defmodule Ecto.Adapters.FoundationDB.Layer.IndexInventory do
{max_version, idxs, fn -> true end}
end

@doc """
## Examples
iex> Ecto.Adapters.FoundationDB.Layer.IndexInventory.source_range_startswith([], "users")
"\\xFE\\xFFindexes/users/"
"""
def source_range_startswith(adapter_opts, source) do
Pack.to_raw_fdb_key(adapter_opts, [source(), source, ""])
end

defp cache_lookup(cache?, cache, cache_key, now) do
case {cache?, :ets.lookup(cache, cache_key)} do
{true, [item]} ->
Expand Down
66 changes: 13 additions & 53 deletions lib/ecto/adapters/foundationdb/layer/indexer/default.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.Default do
alias Ecto.Adapters.FoundationDB.Exception.Unsupported
alias Ecto.Adapters.FoundationDB.Layer.Indexer
alias Ecto.Adapters.FoundationDB.Layer.Pack
alias Ecto.Adapters.FoundationDB.Options
alias Ecto.Adapters.FoundationDB.QueryPlan

@behaviour Indexer

@index_namespace "i"

@doc """
In the index key, values must be encoded into a fixed-length binary.
Fixed-length is required so that get_range can be used reliably in the presence of
arbitrary data. In a naive approach, the key_delimiter can conflict with
the bytes included in the index value.
arbitrary data.
However, this means our indexes will have conflicts that must be resolved with
filtering.
Expand Down Expand Up @@ -63,12 +59,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.Default do
index_fields = idx[:fields]
options = idx[:options]

key_startswith = Pack.to_fdb_datakey_startswith(adapter_opts, source)
key_start = key_startswith
key_end = :erlfdb_key.strinc(key_startswith)

# Prevent updates on the keys so that we write the correct index values
:erlfdb.add_write_conflict_range(tx, key_start, key_end)
{key_start, key_end} = Pack.primary_range(source)

# Write the actual index for any existing data in this tenant
#
Expand Down Expand Up @@ -141,31 +132,23 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.Default do
"""
end

def range(idx, %{opts: adapter_opts}, plan = %QueryPlan.Equal{}, options) do
start_key =
to_fdb_indexkey(
adapter_opts,
idx[:options],
plan.source,
idx[:id],
[plan.param],
nil
)
def range(idx, _adapter_meta, plan = %QueryPlan.Equal{}, options) do
index_values = for val <- [plan.param], do: indexkey_encoder(val, idx[:options])
{start_key, end_key} = Pack.default_index_range(plan.source, idx[:id], index_values)

end_key = :erlfdb_key.strinc(start_key)
start_key = options[:start_key] || start_key

{start_key, end_key}
end

def range(idx, %{opts: adapter_opts}, plan = %QueryPlan.Between{}, options) do
def range(idx, _adapter_meta, plan = %QueryPlan.Between{}, options) do
index_options = idx[:options]

case Keyword.get(index_options, :indexer, nil) do
:timeseries ->
[start_key, end_key] =
for x <- [plan.param_left, plan.param_right],
do: to_fdb_indexkey(adapter_opts, idx[:options], plan.source, idx[:id], [x], nil)
do: Pack.default_index_pack(plan.source, idx[:id], ["#{x}"], nil)

start_key = options[:start_key] || start_key

Expand Down Expand Up @@ -205,7 +188,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.Default do

# Note: pk is always first. See insert and update paths
defp get_index_entry(
adapter_opts,
_adapter_opts,
{fdb_key, data_object = [{pk_field, pk_value} | _]},
index_fields,
index_options,
Expand All @@ -214,36 +197,13 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.Default do
) do
index_fields = index_fields -- [pk_field]

index_entries =
for idx_field <- index_fields, do: {idx_field, Keyword.get(data_object, idx_field)}

{_, path_vals} = Enum.unzip(index_entries)
index_values =
for idx_field <- index_fields do
indexkey_encoder(Keyword.get(data_object, idx_field), index_options)
end

index_key =
to_fdb_indexkey(
adapter_opts,
index_options,
source,
"#{index_name}",
path_vals,
pk_value
)
index_key = Pack.default_index_pack(source, index_name, index_values, pk_value)

{index_key, Indexer.pack({fdb_key, data_object})}
end

# iex> Ecto.Adapters.FoundationDB.Layer.Indexer.Default.to_fdb_indexkey([], [],
# ...> "table", "my_index", ["abc", "123"], "my-pk-id")
# "\\xFEtable/i/my_index/M\\xDA\\xD8\\xFB/V\\xE3\\xD1\\x01/\\x83m\\0\\0\\0\\bmy-pk-id"
defp to_fdb_indexkey(adapter_opts, index_options, source, index_name, vals, id)
when is_list(vals) do
fun = Options.get(adapter_opts, :indexkey_encoder)
vals = for v <- vals, do: fun.(v, index_options)

Pack.to_raw_fdb_key(
adapter_opts,
[source, @index_namespace, index_name | vals] ++
if(is_nil(id), do: [], else: [Pack.encode_pk_for_key(id)])
)
end
end
21 changes: 9 additions & 12 deletions lib/ecto/adapters/foundationdb/layer/indexer/max_value.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,39 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.MaxValue do
def decode(:not_found), do: -1
def decode(x), do: :binary.decode_unsigned(x, :little)

def create(tx, idx, %{opts: adapter_opts}) do
def create(tx, idx, _adapter_meta) do
index_name = idx[:id]
source = idx[:source]
[max_field] = idx[:fields]

key_startswith = Pack.to_fdb_datakey_startswith(adapter_opts, source)
key_start = key_startswith
key_end = :erlfdb_key.strinc(key_startswith)
:erlfdb.add_write_conflict_range(tx, key_start, key_end)
{key_start, key_end} = Pack.primary_range(source)

tx
|> :erlfdb.get_range(key_start, key_end)
|> :erlfdb.wait()
|> Enum.each(fn {_fdb_key, fdb_value} ->
data = Pack.from_fdb_value(fdb_value)
key = key(adapter_opts, source, index_name)
key = key(source, index_name)
val = data[max_field]
:erlfdb.max(tx, key, val)
end)
end

def set(tx, idx, %{opts: adapter_opts}, {_, data}) do
def set(tx, idx, _adapter_meta, {_, data}) do
index_name = idx[:id]
source = idx[:source]
[max_field] = idx[:fields]
key = key(adapter_opts, source, index_name)
key = key(source, index_name)
val = data[max_field]
:erlfdb.max(tx, key, val)
end

def clear(tx, idx, adapter_meta = %{opts: adapter_opts}, {_, data}) do
def clear(tx, idx, adapter_meta, {_, data}) do
index_name = idx[:id]
source = idx[:source]
[max_field] = idx[:fields]
val = data[max_field]
key = key(adapter_opts, source, index_name)
key = key(source, index_name)

db_val =
tx
Expand All @@ -68,7 +65,7 @@ defmodule Ecto.Adapters.FoundationDB.Layer.Indexer.MaxValue do
"""
end

def key(adapter_opts, source, index_name) do
Pack.to_raw_fdb_key(adapter_opts, [source, "max", index_name])
def key(source, index_name) do
Pack.namespaced_pack(source, "max", ["#{index_name}"])
end
end
Loading

0 comments on commit 841ed79

Please sign in to comment.